diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneLeaderboardScore.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapLeaderboardScore.cs similarity index 76% rename from osu.Game.Tests/Visual/SongSelectV2/TestSceneLeaderboardScore.cs rename to osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapLeaderboardScore.cs index 9d827fdc72..1b6d56df16 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestSceneLeaderboardScore.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapLeaderboardScore.cs @@ -7,6 +7,7 @@ using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Cursor; using osu.Framework.Testing; using osu.Framework.Utils; using osu.Game.Configuration; @@ -28,7 +29,7 @@ using osuTK.Input; namespace osu.Game.Tests.Visual.SongSelectV2 { - public partial class TestSceneLeaderboardScore : SongSelectComponentsTestScene + public partial class TestSceneBeatmapLeaderboardScore : SongSelectComponentsTestScene { [Cached] private OverlayColourProvider colourProvider { get; set; } = new OverlayColourProvider(OverlayColourScheme.Aquamarine); @@ -44,18 +45,23 @@ namespace osu.Game.Tests.Visual.SongSelectV2 { AddStep("create content", () => { - Children = new Drawable[] + Child = new PopoverContainer { - fillFlow = new FillFlowContainer + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Children = new Drawable[] { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Spacing = new Vector2(0f, 2f), - Shear = OsuGame.SHEAR, - }, - drawWidthText = new OsuSpriteText(), + fillFlow = new FillFlowContainer + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Spacing = new Vector2(0f, 2f), + Shear = OsuGame.SHEAR, + }, + drawWidthText = new OsuSpriteText(), + } }; foreach (var scoreInfo in getTestScores()) @@ -78,22 +84,27 @@ namespace osu.Game.Tests.Visual.SongSelectV2 { AddStep("create content", () => { - Children = new Drawable[] + Child = new PopoverContainer { - fillFlow = new FillFlowContainer + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Children = new Drawable[] { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Spacing = new Vector2(0f, 2f), - }, - drawWidthText = new OsuSpriteText(), + fillFlow = new FillFlowContainer + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Spacing = new Vector2(0f, 2f), + }, + drawWidthText = new OsuSpriteText(), + } }; foreach (var scoreInfo in getTestScores()) { - fillFlow.Add(new BeatmapLeaderboardScore(scoreInfo) + fillFlow.Add(new BeatmapLeaderboardScore(scoreInfo, sheared: false) { Rank = scoreInfo.Position, IsPersonalBest = scoreInfo.User.Id == 2, @@ -112,18 +123,23 @@ namespace osu.Game.Tests.Visual.SongSelectV2 AddStep("create content", () => { - Children = new Drawable[] + Child = new PopoverContainer { - fillFlow = new FillFlowContainer + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Children = new Drawable[] { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Spacing = new Vector2(0f, 2f), - Shear = OsuGame.SHEAR, - }, - drawWidthText = new OsuSpriteText(), + fillFlow = new FillFlowContainer + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Spacing = new Vector2(0f, 2f), + Shear = OsuGame.SHEAR, + }, + drawWidthText = new OsuSpriteText(), + } }; var scoreInfo = new ScoreInfo @@ -260,9 +276,10 @@ namespace osu.Game.Tests.Visual.SongSelectV2 scores[2].TotalScore = RNG.Next(120_000, 400_000); scores[2].MaximumStatistics[HitResult.Great] = 3000; - scores[1].Mods = new Mod[] { new OsuModHidden(), new OsuModDoubleTime(), new OsuModHardRock(), new OsuModFlashlight() }; + scores[1].Mods = new Mod[] { new OsuModHidden(), new OsuModDoubleTime { SpeedChange = { Value = 2 } }, new OsuModHardRock(), new OsuModFlashlight() }; scores[2].Mods = new Mod[] { new OsuModHidden(), new OsuModDoubleTime(), new OsuModHardRock(), new OsuModFlashlight(), new OsuModClassic() }; - scores[3].Mods = new Mod[] { new OsuModHidden(), new OsuModDoubleTime(), new OsuModHardRock(), new OsuModFlashlight(), new OsuModClassic(), new OsuModDifficultyAdjust() }; + scores[3].Mods = new Mod[] + { new OsuModHidden(), new OsuModDoubleTime(), new OsuModHardRock(), new OsuModFlashlight { ComboBasedSize = { Value = false } }, new OsuModClassic(), new OsuModDifficultyAdjust { CircleSize = { Value = 3.2f } } }; scores[4].Mods = new ManiaRuleset().CreateAllMods().ToArray(); return scores; diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapLeaderboardWedge.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapLeaderboardWedge.cs new file mode 100644 index 0000000000..61d23c4513 --- /dev/null +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapLeaderboardWedge.cs @@ -0,0 +1,370 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Collections.Generic; +using System.Linq; +using NUnit.Framework; +using osu.Framework.Allocation; +using osu.Framework.Audio; +using osu.Framework.Bindables; +using osu.Framework.Extensions; +using osu.Framework.Extensions.ObjectExtensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Platform; +using osu.Framework.Testing; +using osu.Game.Beatmaps; +using osu.Game.Graphics.Cursor; +using osu.Game.Graphics.UserInterface; +using osu.Game.Online.API.Requests.Responses; +using osu.Game.Online.Leaderboards; +using osu.Game.Overlays; +using osu.Game.Rulesets; +using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Osu; +using osu.Game.Rulesets.Osu.Mods; +using osu.Game.Scoring; +using osu.Game.Screens.Select.Leaderboards; +using osu.Game.Screens.SelectV2; +using osu.Game.Tests.Resources; +using osu.Game.Tests.Visual.SongSelect; +using osu.Game.Users; +using osuTK.Input; + +namespace osu.Game.Tests.Visual.SongSelectV2 +{ + public partial class TestSceneBeatmapLeaderboardWedge : SongSelectComponentsTestScene + { + [Cached] + private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Aquamarine); + + private TestBeatmapLeaderboardWedge leaderboard = null!; + private ScoreManager scoreManager = null!; + private RulesetStore rulesetStore = null!; + private BeatmapManager beatmapManager = null!; + private OsuContextMenuContainer contentContainer = null!; + private DialogOverlay dialogOverlay = null!; + + private LeaderboardManager leaderboardManager = null!; + + protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) + { + var dependencies = new DependencyContainer(base.CreateChildDependencies(parent)); + + dependencies.Cache(rulesetStore = new RealmRulesetStore(Realm)); + dependencies.Cache(beatmapManager = new BeatmapManager(LocalStorage, Realm, null, dependencies.Get(), Resources, dependencies.Get(), Beatmap.Default)); + dependencies.Cache(scoreManager = new ScoreManager(rulesetStore, () => beatmapManager, LocalStorage, Realm, API)); + dependencies.Cache(leaderboardManager = new LeaderboardManager()); + + Dependencies.Cache(Realm); + + return dependencies; + } + + [BackgroundDependencyLoader] + private void load() + { + LoadComponent(dialogOverlay = new DialogOverlay + { + Depth = -1 + }); + + LoadComponent(leaderboardManager); + + Child = contentContainer = new OsuContextMenuContainer + { + RelativeSizeAxes = Axes.X, + Height = 500, + Children = new Drawable[] + { + dialogOverlay, + } + }; + + AddSliderStep("change relative height", 0f, 1f, 0.65f, v => Schedule(() => + { + contentContainer.Height = v * DrawHeight; + })); + } + + [SetUp] + public void SetUp() => Schedule(() => + { + if (leaderboard.IsNotNull()) + contentContainer.Remove(leaderboard, false); + + contentContainer.Add(leaderboard = new TestBeatmapLeaderboardWedge + { + RelativeSizeAxes = Axes.Both, + State = { Value = Visibility.Visible }, + }); + }); + + [SetUpSteps] + public override void SetUpSteps() + { + base.SetUpSteps(); + } + + [Test] + public void TestPersonalBest() + { + AddStep(@"Show personal best", showPersonalBest); + } + + [Test] + public void TestGlobalScoresDisplay() + { + setScope(BeatmapLeaderboardScope.Global); + + AddStep(@"New Scores", () => leaderboard.SetScores(TestSceneBeatmapLeaderboard.GenerateSampleScores(new BeatmapInfo()))); + AddStep(@"New Scores with teams", () => leaderboard.SetScores(TestSceneBeatmapLeaderboard.GenerateSampleScores(new BeatmapInfo()).Select(s => + { + s.User.Team = new APITeam(); + return s; + }))); + } + + [Test] + public void TestPersonalBestWithNullPosition() + { + AddStep("null personal best position", showPersonalBestWithNullPosition); + } + + [Test] + public void TestPlaceholderStates() + { + AddStep("ensure no scores displayed", () => leaderboard.SetScores(Array.Empty())); + + AddStep(@"Retrieving", () => leaderboard.SetState(LeaderboardState.Retrieving)); + AddStep(@"Network failure", () => leaderboard.SetState(LeaderboardState.NetworkFailure)); + AddStep(@"No team", () => leaderboard.SetState(LeaderboardState.NoTeam)); + AddStep(@"No supporter", () => leaderboard.SetState(LeaderboardState.NotSupporter)); + AddStep(@"Not logged in", () => leaderboard.SetState(LeaderboardState.NotLoggedIn)); + AddStep(@"Ruleset unavailable", () => leaderboard.SetState(LeaderboardState.RulesetUnavailable)); + AddStep(@"Beatmap unavailable", () => leaderboard.SetState(LeaderboardState.BeatmapUnavailable)); + AddStep(@"None selected", () => leaderboard.SetState(LeaderboardState.NoneSelected)); + } + + [Test] + public void TestUseTheseModsDoesNotCopySystemMods() + { + AddStep(@"set scores", () => leaderboard.SetScores(TestSceneBeatmapLeaderboard.GenerateSampleScores(new BeatmapInfo()), new ScoreInfo + { + OnlineID = 1337, + Position = 999, + Rank = ScoreRank.XH, + Accuracy = 1, + MaxCombo = 244, + TotalScore = 1707827, + Ruleset = new OsuRuleset().RulesetInfo, + Mods = new Mod[] { new OsuModHidden(), new ModScoreV2(), }, + User = new APIUser + { + Id = 6602580, + Username = @"waaiiru", + CountryCode = CountryCode.ES, + } + })); + AddUntilStep("wait for scores", () => this.ChildrenOfType().Count(), () => Is.GreaterThan(0)); + AddStep("right click panel", () => + { + InputManager.MoveMouseTo(this.ChildrenOfType().Last()); + InputManager.Click(MouseButton.Right); + }); + AddStep("click use these mods", () => + { + InputManager.MoveMouseTo(this.ChildrenOfType().First()); + InputManager.Click(MouseButton.Left); + }); + AddAssert("received HD", () => this.ChildrenOfType().Last().SelectedMods.Value.Any(m => m is OsuModHidden)); + AddAssert("did not receive SV2", () => !this.ChildrenOfType().Last().SelectedMods.Value.Any(m => m is ModScoreV2)); + } + + [Test] + public void TestLocalScoresDisplay() + { + BeatmapInfo beatmapInfo = null!; + + setScope(BeatmapLeaderboardScope.Local); + + AddStep(@"Set beatmap", () => + { + beatmapManager.Import(TestResources.GetQuickTestBeatmapForImport()).WaitSafely(); + beatmapInfo = beatmapManager.GetAllUsableBeatmapSets().First().Beatmaps.First(); + + Beatmap.Value = beatmapManager.GetWorkingBeatmap(beatmapInfo); + }); + + clearScores(); + checkDisplayedCount(0); + + importMoreScores(() => beatmapInfo); + checkDisplayedCount(10); + + importMoreScores(() => beatmapInfo); + checkDisplayedCount(20); + + clearScores(); + checkDisplayedCount(0); + } + + [Test] + public void TestLocalScoresDisplayWorksWhenStartingOffline() + { + BeatmapInfo beatmapInfo = null!; + + AddStep("Log out", () => API.Logout()); + setScope(BeatmapLeaderboardScope.Local); + + AddStep(@"Import beatmap", () => + { + beatmapManager.Import(TestResources.GetQuickTestBeatmapForImport()).WaitSafely(); + beatmapInfo = beatmapManager.GetAllUsableBeatmapSets().First().Beatmaps.First(); + + Beatmap.Value = beatmapManager.GetWorkingBeatmap(beatmapInfo); + }); + + clearScores(); + importMoreScores(() => beatmapInfo); + checkDisplayedCount(10); + } + + [Test] + public void TestLocalScoresDisplayOnBeatmapEdit() + { + BeatmapInfo beatmapInfo = null!; + string originalHash = string.Empty; + + setScope(BeatmapLeaderboardScope.Local); + + AddStep(@"Import beatmap", () => + { + beatmapManager.Import(TestResources.GetQuickTestBeatmapForImport()).WaitSafely(); + beatmapInfo = beatmapManager.GetAllUsableBeatmapSets().First().Beatmaps.First(); + + Beatmap.Value = beatmapManager.GetWorkingBeatmap(beatmapInfo); + }); + + clearScores(); + checkDisplayedCount(0); + + AddStep(@"Perform initial save to guarantee stable hash", () => + { + IBeatmap beatmap = beatmapManager.GetWorkingBeatmap(beatmapInfo).Beatmap; + beatmapManager.Save(beatmapInfo, beatmap); + + originalHash = beatmapInfo.Hash; + }); + + importMoreScores(() => beatmapInfo); + + checkDisplayedCount(10); + checkStoredCount(10); + + AddStep(@"Save with changes", () => + { + IBeatmap beatmap = beatmapManager.GetWorkingBeatmap(beatmapInfo).Beatmap; + beatmap.Difficulty.ApproachRate = 12; + beatmapManager.Save(beatmapInfo, beatmap); + }); + + AddAssert("Hash changed", () => beatmapInfo.Hash, () => Is.Not.EqualTo(originalHash)); + checkDisplayedCount(0); + checkStoredCount(10); + + importMoreScores(() => beatmapInfo); + importMoreScores(() => beatmapInfo); + checkDisplayedCount(20); + checkStoredCount(30); + + AddStep(@"Revert changes", () => + { + IBeatmap beatmap = beatmapManager.GetWorkingBeatmap(beatmapInfo).Beatmap; + beatmap.Difficulty.ApproachRate = 8; + beatmapManager.Save(beatmapInfo, beatmap); + }); + + AddAssert("Hash restored", () => beatmapInfo.Hash, () => Is.EqualTo(originalHash)); + checkDisplayedCount(10); + checkStoredCount(30); + + clearScores(); + checkDisplayedCount(0); + checkStoredCount(0); + } + + private void showPersonalBestWithNullPosition() + { + leaderboard.SetScores(TestSceneBeatmapLeaderboard.GenerateSampleScores(new BeatmapInfo()), new ScoreInfo + { + OnlineID = 1337, + Rank = ScoreRank.XH, + Accuracy = 1, + MaxCombo = 244, + TotalScore = 1707827, + Mods = new Mod[] { new OsuModHidden(), new OsuModHardRock() }, + Ruleset = new OsuRuleset().RulesetInfo, + User = new APIUser + { + Id = 6602580, + Username = @"waaiiru", + CountryCode = CountryCode.ES, + }, + }); + } + + private void showPersonalBest() + { + leaderboard.SetScores(TestSceneBeatmapLeaderboard.GenerateSampleScores(new BeatmapInfo()), new ScoreInfo + { + OnlineID = 1337, + Position = 999, + Rank = ScoreRank.XH, + Accuracy = 1, + MaxCombo = 244, + TotalScore = 1707827, + Ruleset = new OsuRuleset().RulesetInfo, + Mods = new Mod[] { new OsuModHidden(), new OsuModHardRock() }, + User = new APIUser + { + Id = 6602580, + Username = @"waaiiru", + CountryCode = CountryCode.ES, + } + }); + } + + private void setScope(BeatmapLeaderboardScope scope) + { + AddStep(@"Set scope", () => ((Bindable)leaderboard.Scope).Value = scope); + } + + private void importMoreScores(Func beatmapInfo) + { + AddStep(@"Import new scores", () => + { + foreach (var score in TestSceneBeatmapLeaderboard.GenerateSampleScores(beatmapInfo())) + scoreManager.Import(score); + }); + } + + private void clearScores() + { + AddStep("Clear all scores", () => scoreManager.Delete()); + } + + private void checkDisplayedCount(int expected) => + AddUntilStep($"{expected} scores displayed", () => leaderboard.ChildrenOfType().Count(), () => Is.EqualTo(expected)); + + private void checkStoredCount(int expected) => + AddUntilStep($"Total scores stored is {expected}", () => Realm.Run(r => r.All().Count(s => !s.DeletePending)), () => Is.EqualTo(expected)); + + private partial class TestBeatmapLeaderboardWedge : BeatmapLeaderboardWedge + { + public new void SetState(LeaderboardState state) => base.SetState(state); + public new void SetScores(IEnumerable scores, ScoreInfo? userScore = null) => base.SetScores(scores, userScore); + } + } +} diff --git a/osu.Game/Screens/SelectV2/BeatmapLeaderboardScore.cs b/osu.Game/Screens/SelectV2/BeatmapLeaderboardScore.cs index c9413a9414..b422a6474e 100644 --- a/osu.Game/Screens/SelectV2/BeatmapLeaderboardScore.cs +++ b/osu.Game/Screens/SelectV2/BeatmapLeaderboardScore.cs @@ -25,7 +25,6 @@ using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; using osu.Game.Online.API; -using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.Leaderboards; using osu.Game.Overlays; using osu.Game.Resources.Localisation.Web; @@ -43,8 +42,10 @@ using CommonStrings = osu.Game.Localisation.CommonStrings; namespace osu.Game.Screens.SelectV2 { - public partial class BeatmapLeaderboardScore : OsuClickableContainer, IHasContextMenu, IHasCustomTooltip + public sealed partial class BeatmapLeaderboardScore : OsuClickableContainer, IHasContextMenu, IHasCustomTooltip { + public const int HEIGHT = 50; + public Bindable> SelectedMods = new Bindable>(); /// @@ -57,24 +58,6 @@ namespace osu.Game.Screens.SelectV2 public int? Rank { get; init; } public bool IsPersonalBest { get; init; } - private const float expanded_right_content_width = 210; - private const float grade_width = 40; - private const float username_min_width = 125; - private const float statistics_regular_min_width = 175; - private const float statistics_compact_min_width = 100; - private const float rank_label_width = 65; - - private readonly ScoreInfo score; - private readonly bool sheared; - - private const int height = 60; - private const int corner_radius = 10; - private const int transition_duration = 200; - - private Colour4 foregroundColour; - private Colour4 backgroundColour; - private ColourInfo totalScoreBackgroundGradient; - [Resolved] private OverlayColourProvider colourProvider { get; set; } = null!; @@ -84,38 +67,55 @@ namespace osu.Game.Screens.SelectV2 [Resolved] private ScoreManager scoreManager { get; set; } = null!; + [Resolved] + private OsuConfigManager config { get; set; } = null!; + [Resolved] private Clipboard? clipboard { get; set; } [Resolved] private IAPIProvider api { get; set; } = null!; - private Container content = null!; + private const float expanded_right_content_width = 200; + private const float grade_width = 35; + 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 int corner_radius = 10; + private const int transition_duration = 200; + + private static readonly Color4 personal_best_gradient_left = Color4Extensions.FromHex("#66FFCC"); + private static readonly Color4 personal_best_gradient_right = Color4Extensions.FromHex("#51A388"); + + private Colour4 foregroundColour; + private Colour4 backgroundColour; + private ColourInfo totalScoreBackgroundGradient; + + private ColourInfo personalBestGradient; + + private IBindable scoringMode { get; set; } = null!; + private Box background = null!; private Box foreground = null!; - private Drawable avatar = null!; private ClickableAvatar innerAvatar = null!; - private OsuSpriteText nameLabel = null!; - private List statisticsLabels = null!; - private Container rightContent = null!; - protected Container RankContainer { get; private set; } = null!; - private FillFlowContainer flagBadgeAndDateContainer = null!; - private FillFlowContainer modsContainer = null!; + private FillFlowContainer modsContainer = null!; - private OsuSpriteText scoreText = null!; - private Drawable scoreRank = null!; private Box totalScoreBackground = null!; private FillFlowContainer statisticsContainer = null!; - private RankLabel rankLabel = null!; + private Container personalBestIndicator = null!; + private Container rankLabelStandalone = null!; private Container rankLabelOverlay = null!; - public ITooltip GetCustomTooltip() => new LeaderboardScoreTooltip(); - public virtual ScoreInfo TooltipContent => score; + private readonly ScoreInfo score; + + private readonly bool sheared; public BeatmapLeaderboardScore(ScoreInfo score, bool sheared = true) { @@ -124,25 +124,18 @@ namespace osu.Game.Screens.SelectV2 Shear = sheared ? OsuGame.SHEAR : Vector2.Zero; RelativeSizeAxes = Axes.X; - Height = height; + Height = HEIGHT; } [BackgroundDependencyLoader] private void load() { - var user = score.User; - - foregroundColour = IsPersonalBest ? colourProvider.Background1 : colourProvider.Background5; - backgroundColour = IsPersonalBest ? colourProvider.Background2 : colourProvider.Background4; + foregroundColour = colourProvider.Background5; + backgroundColour = colourProvider.Background3; totalScoreBackgroundGradient = ColourInfo.GradientHorizontal(backgroundColour.Opacity(0), backgroundColour); + personalBestGradient = ColourInfo.GradientHorizontal(personal_best_gradient_left, personal_best_gradient_right); - statisticsLabels = GetStatistics(score).Select(s => new ScoreComponentLabel(s, score) - { - // ensure statistics container is the correct width when invalidating - AlwaysPresent = true, - }).ToList(); - - Child = content = new Container + Child = new Container { Masking = true, CornerRadius = corner_radius, @@ -167,18 +160,297 @@ namespace osu.Game.Screens.SelectV2 { new Drawable[] { - new Container + rankLabelStandalone = new Container { - AutoSizeAxes = Axes.X, + Width = rank_label_width, RelativeSizeAxes = Axes.Y, - Child = rankLabel = new RankLabel(Rank, sheared) + Children = new Drawable[] { - Width = rank_label_width, - RelativeSizeAxes = Axes.Y, + personalBestIndicator = new Container + { + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding { Right = -10f }, + Alpha = IsPersonalBest ? 1 : 0, + Colour = personalBestGradient, + Child = new Box { RelativeSizeAxes = Axes.Both }, + }, + new RankLabel(Rank, sheared, darkText: IsPersonalBest) + { + RelativeSizeAxes = Axes.Both, + } }, }, - createCentreContent(user), - createRightContent() + new Container + { + Name = @"Centre container", + Masking = true, + CornerRadius = corner_radius, + RelativeSizeAxes = Axes.Both, + Children = new Drawable[] + { + foreground = new Box + { + RelativeSizeAxes = Axes.Both, + Colour = foregroundColour + }, + new UserCoverBackground + { + RelativeSizeAxes = Axes.Both, + User = score.User, + Shear = sheared ? -OsuGame.SHEAR : Vector2.Zero, + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + Colour = ColourInfo.GradientHorizontal(Colour4.White.Opacity(0.5f), Colour4.FromHex(@"222A27").Opacity(1)), + }, + new GridContainer + { + RelativeSizeAxes = Axes.Both, + ColumnDimensions = new[] + { + new Dimension(GridSizeMode.AutoSize), + new Dimension(), + new Dimension(GridSizeMode.AutoSize), + }, + Content = new[] + { + new Drawable[] + { + new Container + { + AutoSizeAxes = Axes.Both, + CornerRadius = corner_radius, + Masking = true, + Children = new Drawable[] + { + new DelayedLoadWrapper(innerAvatar = new ClickableAvatar(score.User) + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Scale = new Vector2(1.1f), + Shear = sheared ? -OsuGame.SHEAR : Vector2.Zero, + RelativeSizeAxes = Axes.Both, + }) + { + RelativeSizeAxes = Axes.None, + Size = new Vector2(HEIGHT) + }, + rankLabelOverlay = new Container + { + RelativeSizeAxes = Axes.Both, + Alpha = 0, + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = Colour4.Black.Opacity(0.5f), + }, + new RankLabel(Rank, sheared, false) + { + AutoSizeAxes = Axes.Both, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + }, + } + } + }, + }, + new FillFlowContainer + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Vertical, + Padding = new MarginPadding { Horizontal = corner_radius }, + Children = new Drawable[] + { + new FillFlowContainer + { + Shear = sheared ? -OsuGame.SHEAR : Vector2.Zero, + Direction = FillDirection.Horizontal, + Spacing = new Vector2(5), + AutoSizeAxes = Axes.Both, + Masking = true, + Children = new Drawable[] + { + new UpdateableFlag(score.User.CountryCode) + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Size = new Vector2(20, 14), + }, + new UpdateableTeamFlag(score.User.Team) + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Size = new Vector2(30, 15), + }, + new DateLabel(score.Date) + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Colour = colourProvider.Content2, + UseFullGlyphHeight = false, + } + } + }, + new TruncatingSpriteText + { + RelativeSizeAxes = Axes.X, + Shear = sheared ? -OsuGame.SHEAR : Vector2.Zero, + Text = score.User.Username, + Font = OsuFont.Style.Heading2, + } + } + }, + new Container + { + AutoSizeAxes = Axes.Both, + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, + Child = statisticsContainer = new FillFlowContainer + { + Name = @"Statistics container", + Padding = new MarginPadding { Right = 40 }, + Spacing = new Vector2(25, 0), + Shear = sheared ? -OsuGame.SHEAR : Vector2.Zero, + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Horizontal, + Children = getStatistics(score).Select(s => new ScoreComponentLabel(s, score)).ToList(), + Alpha = 0, + } + } + } + }, + }, + }, + }, + rightContent = new Container + { + Name = @"Right content", + RelativeSizeAxes = Axes.Y, + Child = new Container + { + RelativeSizeAxes = Axes.Both, + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight, + Children = new Drawable[] + { + new Container + { + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding { Right = grade_width }, + Child = new Box + { + RelativeSizeAxes = Axes.Both, + Colour = ColourInfo.GradientHorizontal(backgroundColour.Opacity(0), OsuColour.ForRank(score.Rank)), + }, + }, + new Box + { + RelativeSizeAxes = Axes.Y, + Width = grade_width, + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight, + Colour = OsuColour.ForRank(score.Rank), + }, + new TrianglesV2 + { + RelativeSizeAxes = Axes.Both, + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight, + SpawnRatio = 2, + Velocity = 0.7f, + Colour = ColourInfo.GradientHorizontal(backgroundColour.Opacity(0), OsuColour.ForRank(score.Rank).Darken(0.2f)), + }, + new Container + { + Shear = sheared ? -OsuGame.SHEAR : Vector2.Zero, + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, + RelativeSizeAxes = Axes.Y, + Width = grade_width, + Child = new OsuSpriteText + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Spacing = new Vector2(-2), + Colour = DrawableRank.GetRankNameColour(score.Rank), + Font = OsuFont.Numeric.With(size: 14), + Text = DrawableRank.GetRankName(score.Rank), + ShadowColour = Color4.Black.Opacity(0.3f), + ShadowOffset = new Vector2(0, 0.08f), + Shadow = true, + UseFullGlyphHeight = false, + }, + }, + new Container + { + RelativeSizeAxes = Axes.Both, + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Padding = new MarginPadding { Right = grade_width }, + Child = new Container + { + RelativeSizeAxes = Axes.Both, + Masking = true, + CornerRadius = corner_radius, + Children = new Drawable[] + { + totalScoreBackground = new Box + { + RelativeSizeAxes = Axes.Both, + Colour = totalScoreBackgroundGradient, + }, + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = ColourInfo.GradientHorizontal(backgroundColour.Opacity(0), OsuColour.ForRank(score.Rank).Opacity(0.5f)), + }, + new FillFlowContainer + { + AutoSizeAxes = Axes.Both, + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, + Direction = FillDirection.Vertical, + Padding = new MarginPadding { Horizontal = corner_radius }, + Spacing = new Vector2(0f, -2f), + Children = new Drawable[] + { + new OsuSpriteText + { + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight, + UseFullGlyphHeight = false, + Current = scoreManager.GetBindableTotalScoreString(score), + Spacing = new Vector2(-1.5f), + Font = OsuFont.Style.Subtitle.With(weight: FontWeight.Light, fixedWidth: true), + Shear = sheared ? -OsuGame.SHEAR : Vector2.Zero, + }, + new InputBlockingContainer + { + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight, + AutoSizeAxes = Axes.Both, + Child = modsContainer = new FillFlowContainer + { + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Horizontal, + Spacing = new Vector2(-10, 0), + Shear = sheared ? -OsuGame.SHEAR : Vector2.Zero, + }, + }, + } + } + } + } + } + } + }, + } } } } @@ -188,11 +460,6 @@ namespace osu.Game.Screens.SelectV2 innerAvatar.OnLoadComplete += d => d.FadeInFromZero(200); } - [Resolved] - private OsuConfigManager config { get; set; } = null!; - - private IBindable scoringMode { get; set; } = null!; - protected override void LoadComplete() { base.LoadComplete(); @@ -203,7 +470,7 @@ namespace osu.Game.Screens.SelectV2 switch (s.NewValue) { case ScoringMode.Standardised: - rightContent.Width = 180f; + rightContent.Width = 170; break; case ScoringMode.Classic: @@ -217,342 +484,24 @@ namespace osu.Game.Screens.SelectV2 private void updateModDisplay() { - int maxMods = scoringMode.Value == ScoringMode.Standardised ? 4 : 5; - if (score.Mods.Length > 0) { modsContainer.Padding = new MarginPadding { Top = 4f }; - modsContainer.ChildrenEnumerable = score.Mods.AsOrdered().Take(Math.Min(maxMods, score.Mods.Length)).Select(mod => new ColouredModSwitchTiny(mod) + modsContainer.ChildrenEnumerable = score.Mods.AsOrdered().Select(mod => new ModIcon(mod) { - Scale = new Vector2(0.375f) + Scale = new Vector2(0.3f), + // trim mod icon height down to its true height for alignment purposes. + Height = ModIcon.MOD_ICON_SIZE.Y * 3 / 4f, }); - - if (score.Mods.Length > maxMods) - { - modsContainer.Remove(modsContainer[^1], true); - modsContainer.Add(new MoreModSwitchTiny(score.Mods.Length - maxMods + 1) - { - Scale = new Vector2(0.375f), - }); - } } } - private Container createCentreContent(APIUser user) => new Container - { - Name = @"Centre container", - Masking = true, - CornerRadius = corner_radius, - RelativeSizeAxes = Axes.Both, - Children = new Drawable[] - { - foreground = new Box - { - RelativeSizeAxes = Axes.Both, - Colour = foregroundColour - }, - new UserCoverBackground - { - RelativeSizeAxes = Axes.Both, - User = score.User, - Shear = sheared ? -OsuGame.SHEAR : Vector2.Zero, - Anchor = Anchor.BottomLeft, - Origin = Anchor.BottomLeft, - Colour = ColourInfo.GradientHorizontal(Colour4.White.Opacity(0.5f), Colour4.FromHex(@"222A27").Opacity(1)), - }, - new GridContainer - { - RelativeSizeAxes = Axes.Both, - ColumnDimensions = new[] - { - new Dimension(GridSizeMode.AutoSize), - new Dimension(), - new Dimension(GridSizeMode.AutoSize), - }, - Content = new[] - { - new Drawable[] - { - new Container - { - AutoSizeAxes = Axes.Both, - CornerRadius = corner_radius, - Masking = true, - Children = new[] - { - avatar = new DelayedLoadWrapper( - innerAvatar = new ClickableAvatar(user) - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Scale = new Vector2(1.1f), - Shear = sheared ? -OsuGame.SHEAR : Vector2.Zero, - RelativeSizeAxes = Axes.Both, - }) - { - RelativeSizeAxes = Axes.None, - Size = new Vector2(height) - }, - rankLabelOverlay = new Container - { - RelativeSizeAxes = Axes.Both, - Alpha = 0, - Children = new Drawable[] - { - new Box - { - RelativeSizeAxes = Axes.Both, - Colour = Colour4.Black.Opacity(0.5f), - }, - new RankLabel(Rank, sheared) - { - AutoSizeAxes = Axes.Both, - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - }, - } - } - }, - }, - new FillFlowContainer - { - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Direction = FillDirection.Vertical, - Padding = new MarginPadding { Horizontal = corner_radius }, - Children = new Drawable[] - { - flagBadgeAndDateContainer = new FillFlowContainer - { - Shear = sheared ? -OsuGame.SHEAR : Vector2.Zero, - Direction = FillDirection.Horizontal, - Spacing = new Vector2(5), - AutoSizeAxes = Axes.Both, - Masking = true, - Children = new Drawable[] - { - new UpdateableFlag(user.CountryCode) - { - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - Size = new Vector2(24, 16), - }, - new UpdateableTeamFlag(user.Team) - { - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - Size = new Vector2(40, 20), - }, - new DateLabel(score.Date) - { - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - UseFullGlyphHeight = false, - } - } - }, - nameLabel = new TruncatingSpriteText - { - RelativeSizeAxes = Axes.X, - Shear = sheared ? -OsuGame.SHEAR : Vector2.Zero, - Text = user.Username, - Font = OsuFont.GetFont(size: 20, weight: FontWeight.SemiBold) - } - } - }, - new Container - { - AutoSizeAxes = Axes.Both, - Anchor = Anchor.CentreRight, - Origin = Anchor.CentreRight, - Child = statisticsContainer = new FillFlowContainer - { - Name = @"Statistics container", - Padding = new MarginPadding { Right = 40 }, - Spacing = new Vector2(25, 0), - Shear = sheared ? -OsuGame.SHEAR : Vector2.Zero, - Anchor = Anchor.CentreRight, - Origin = Anchor.CentreRight, - AutoSizeAxes = Axes.Both, - Direction = FillDirection.Horizontal, - Children = statisticsLabels, - Alpha = 0, - LayoutEasing = Easing.OutQuint, - LayoutDuration = transition_duration, - } - } - } - }, - }, - }, - }; - - private Container createRightContent() => rightContent = new Container - { - Name = @"Right content", - RelativeSizeAxes = Axes.Y, - Child = new Container - { - AutoSizeAxes = Axes.X, - RelativeSizeAxes = Axes.Y, - Anchor = Anchor.TopRight, - Origin = Anchor.TopRight, - Children = new Drawable[] - { - new Container - { - RelativeSizeAxes = Axes.Both, - Padding = new MarginPadding { Right = grade_width }, - Child = new Box - { - RelativeSizeAxes = Axes.Both, - Colour = ColourInfo.GradientHorizontal(backgroundColour.Opacity(0), OsuColour.ForRank(score.Rank)), - }, - }, - new Box - { - RelativeSizeAxes = Axes.Y, - Width = grade_width, - Anchor = Anchor.TopRight, - Origin = Anchor.TopRight, - Colour = OsuColour.ForRank(score.Rank), - }, - new TrianglesV2 - { - RelativeSizeAxes = Axes.Both, - Anchor = Anchor.TopRight, - Origin = Anchor.TopRight, - SpawnRatio = 2, - Velocity = 0.7f, - Colour = ColourInfo.GradientHorizontal(backgroundColour.Opacity(0), OsuColour.ForRank(score.Rank).Darken(0.2f)), - }, - RankContainer = new Container - { - Shear = sheared ? -OsuGame.SHEAR : Vector2.Zero, - Anchor = Anchor.CentreRight, - Origin = Anchor.CentreRight, - RelativeSizeAxes = Axes.Y, - Width = grade_width, - Child = scoreRank = new OsuSpriteText - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Spacing = new Vector2(-2), - Colour = DrawableRank.GetRankNameColour(score.Rank), - Font = OsuFont.Numeric.With(size: 16), - Text = DrawableRank.GetRankName(score.Rank), - ShadowColour = Color4.Black.Opacity(0.3f), - ShadowOffset = new Vector2(0, 0.08f), - Shadow = true, - UseFullGlyphHeight = false, - }, - }, - new Container - { - AutoSizeAxes = Axes.X, - RelativeSizeAxes = Axes.Y, - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - Padding = new MarginPadding { Right = grade_width }, - Child = new Container - { - AutoSizeAxes = Axes.X, - RelativeSizeAxes = Axes.Y, - Masking = true, - CornerRadius = corner_radius, - Children = new Drawable[] - { - totalScoreBackground = new Box - { - RelativeSizeAxes = Axes.Both, - Colour = totalScoreBackgroundGradient, - }, - new Box - { - RelativeSizeAxes = Axes.Both, - Colour = ColourInfo.GradientHorizontal(backgroundColour.Opacity(0), OsuColour.ForRank(score.Rank).Opacity(0.5f)), - }, - new FillFlowContainer - { - AutoSizeAxes = Axes.Both, - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - Direction = FillDirection.Vertical, - Padding = new MarginPadding { Horizontal = corner_radius }, - Children = new Drawable[] - { - scoreText = new OsuSpriteText - { - Anchor = Anchor.TopRight, - Origin = Anchor.TopRight, - UseFullGlyphHeight = false, - Shear = sheared ? -OsuGame.SHEAR : Vector2.Zero, - Current = scoreManager.GetBindableTotalScoreString(score), - Font = OsuFont.GetFont(size: 30, weight: FontWeight.Light), - }, - modsContainer = new FillFlowContainer - { - Anchor = Anchor.TopRight, - Origin = Anchor.TopRight, - Shear = sheared ? -OsuGame.SHEAR : Vector2.Zero, - AutoSizeAxes = Axes.Both, - Direction = FillDirection.Horizontal, - Spacing = new Vector2(2f, 0f), - }, - } - } - } - } - } - } - }, - }; - - protected (CaseTransformableString, LocalisableString DisplayAccuracy)[] GetStatistics(ScoreInfo model) => new[] + private (CaseTransformableString, LocalisableString DisplayAccuracy)[] getStatistics(ScoreInfo model) => new[] { (BeatmapsetsStrings.ShowScoreboardHeadersCombo.ToUpper(), model.MaxCombo.ToString().Insert(model.MaxCombo.ToString().Length, "x")), (BeatmapsetsStrings.ShowScoreboardHeadersAccuracy.ToUpper(), model.DisplayAccuracy), }; - public override void Show() - { - foreach (var d in new[] { avatar, nameLabel, scoreText, scoreRank, flagBadgeAndDateContainer, modsContainer }.Concat(statisticsLabels)) - d.FadeOut(); - - Alpha = 0; - - content.MoveToY(75); - avatar.MoveToX(75); - nameLabel.MoveToX(150); - - this.FadeIn(200); - content.MoveToY(0, 800, Easing.OutQuint); - - using (BeginDelayedSequence(100)) - { - avatar.FadeIn(300, Easing.OutQuint); - nameLabel.FadeIn(350, Easing.OutQuint); - - avatar.MoveToX(0, 300, Easing.OutQuint); - nameLabel.MoveToX(0, 350, Easing.OutQuint); - - using (BeginDelayedSequence(250)) - { - scoreText.FadeIn(200); - scoreRank.FadeIn(200); - - using (BeginDelayedSequence(50)) - { - var drawables = new Drawable[] { flagBadgeAndDateContainer, modsContainer }.Concat(statisticsLabels).ToArray(); - for (int i = 0; i < drawables.Length; i++) - drawables[i].FadeIn(100 + i * 50); - } - } - } - } - protected override bool OnHover(HoverEvent e) { updateState(); @@ -568,10 +517,12 @@ namespace osu.Game.Screens.SelectV2 private void updateState() { var lightenedGradient = ColourInfo.GradientHorizontal(backgroundColour.Opacity(0).Lighten(0.2f), backgroundColour.Lighten(0.2f)); + var personalBestLightenedGradient = ColourInfo.GradientHorizontal(personal_best_gradient_left.Lighten(0.2f), personal_best_gradient_right.Lighten(0.2f)); foreground.FadeColour(IsHovered ? foregroundColour.Lighten(0.2f) : foregroundColour, transition_duration, Easing.OutQuint); background.FadeColour(IsHovered ? backgroundColour.Lighten(0.2f) : backgroundColour, transition_duration, Easing.OutQuint); totalScoreBackground.FadeColour(IsHovered ? lightenedGradient : totalScoreBackgroundGradient, transition_duration, Easing.OutQuint); + personalBestIndicator.FadeColour(IsHovered ? personalBestLightenedGradient : personalBestGradient, transition_duration, Easing.OutQuint); if (IsHovered && currentMode != DisplayMode.Full) rankLabelOverlay.FadeIn(transition_duration, Easing.OutQuint); @@ -589,25 +540,26 @@ namespace osu.Game.Screens.SelectV2 if (currentMode != mode) { + double duration = currentMode == null ? 0 : transition_duration; if (mode >= DisplayMode.Full) - rankLabel.FadeIn(transition_duration, Easing.OutQuint).MoveToX(0, transition_duration, Easing.OutQuint); + rankLabelStandalone.FadeIn(duration, Easing.OutQuint).ResizeWidthTo(rank_label_width, duration, Easing.OutQuint); else - rankLabel.FadeOut(transition_duration, Easing.OutQuint).MoveToX(-rankLabel.DrawWidth, transition_duration, Easing.OutQuint); + rankLabelStandalone.FadeOut(duration, Easing.OutQuint).ResizeWidthTo(0, duration, Easing.OutQuint); if (mode >= DisplayMode.Regular) { - statisticsContainer.FadeIn(transition_duration, Easing.OutQuint).MoveToX(0, transition_duration, Easing.OutQuint); + statisticsContainer.FadeIn(duration, Easing.OutQuint).MoveToX(0, duration, Easing.OutQuint); statisticsContainer.Direction = FillDirection.Horizontal; - statisticsContainer.ScaleTo(1, transition_duration, Easing.OutQuint); + statisticsContainer.ScaleTo(1, duration, Easing.OutQuint); } else if (mode >= DisplayMode.Compact) { - statisticsContainer.FadeIn(transition_duration, Easing.OutQuint).MoveToX(0, transition_duration, Easing.OutQuint); + statisticsContainer.FadeIn(duration, Easing.OutQuint).MoveToX(0, duration, Easing.OutQuint); statisticsContainer.Direction = FillDirection.Vertical; - statisticsContainer.ScaleTo(0.8f, transition_duration, Easing.OutQuint); + statisticsContainer.ScaleTo(0.8f, duration, Easing.OutQuint); } else - statisticsContainer.FadeOut(transition_duration, Easing.OutQuint).MoveToX(statisticsContainer.DrawWidth, transition_duration, Easing.OutQuint); + statisticsContainer.FadeOut(duration, Easing.OutQuint).MoveToX(statisticsContainer.DrawWidth, duration, Easing.OutQuint); currentMode = mode; } @@ -615,19 +567,45 @@ 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 >= HEIGHT + 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 >= HEIGHT + 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 >= HEIGHT + username_min_width + statistics_compact_min_width + expanded_right_content_width) return DisplayMode.Compact; return DisplayMode.Minimal; } - #region Subclasses + ITooltip IHasCustomTooltip.GetCustomTooltip() => new LeaderboardScoreTooltip(colourProvider); + + ScoreInfo IHasCustomTooltip.TooltipContent => score; + + MenuItem[] IHasContextMenu.ContextMenuItems + { + get + { + List items = new List(); + + // system mods should never be copied across regardless of anything. + var copyableMods = score.Mods.Where(m => IsValidMod.Invoke(m) && m.Type != ModType.System).ToArray(); + + if (copyableMods.Length > 0) + items.Add(new OsuMenuItem("Use these mods", MenuItemType.Highlighted, () => SelectedMods.Value = copyableMods)); + + if (score.OnlineID > 0) + items.Add(new OsuMenuItem(CommonStrings.CopyLink, MenuItemType.Standard, () => clipboard?.SetText($@"{api.Endpoints.WebsiteUrl}/scores/{score.OnlineID}"))); + + if (score.Files.Count <= 0) return items.ToArray(); + + items.Add(new OsuMenuItem(CommonStrings.Export, MenuItemType.Standard, () => scoreManager.Export(score))); + items.Add(new OsuMenuItem(Resources.Localisation.Web.CommonStrings.ButtonsDelete, MenuItemType.Destructive, () => dialogOverlay?.Push(new LocalScoreDeleteDialog(score)))); + + return items.ToArray(); + } + } private enum DisplayMode { @@ -642,7 +620,7 @@ namespace osu.Game.Screens.SelectV2 public DateLabel(DateTimeOffset date) : base(date) { - Font = OsuFont.GetFont(size: 16, weight: FontWeight.Medium, italics: true); + Font = OsuFont.Style.Caption1.With(weight: FontWeight.SemiBold); } protected override string Format() => Date.ToShortRelativeTime(TimeSpan.FromSeconds(30)); @@ -677,7 +655,7 @@ namespace osu.Game.Screens.SelectV2 { Colour = colourProvider.Content2, Text = statisticInfo.Name, - Font = OsuFont.GetFont(size: 12, weight: FontWeight.Bold), + Font = OsuFont.Style.Caption2.With(weight: FontWeight.SemiBold), }, value = new OsuSpriteText { @@ -685,7 +663,7 @@ namespace osu.Game.Screens.SelectV2 // since the accuracy is sometimes longer than its name. BypassAutoSizeAxes = Axes.X, Text = statisticInfo.Value, - Font = OsuFont.GetFont(size: 19, weight: FontWeight.Medium), + Font = OsuFont.Style.Body, } } }; @@ -697,108 +675,33 @@ namespace osu.Game.Screens.SelectV2 private partial class RankLabel : Container, IHasTooltip { - public RankLabel(int? rank, bool sheared) + private readonly bool darkText; + private readonly OsuSpriteText text; + + public RankLabel(int? rank, bool sheared, bool darkText) { + this.darkText = darkText; if (rank >= 1000) TooltipText = $"#{rank:N0}"; - Child = new OsuSpriteText + Child = text = new OsuSpriteText { Shear = sheared ? -OsuGame.SHEAR : Vector2.Zero, Anchor = Anchor.Centre, Origin = Anchor.Centre, - Font = OsuFont.GetFont(size: 20, weight: FontWeight.SemiBold, italics: true), - Text = rank == null ? "-" : rank.Value.FormatRank().Insert(0, "#") + Font = OsuFont.Style.Heading2, + Text = rank == null ? "-" : rank.Value.FormatRank().Insert(0, "#"), + Shadow = !darkText, }; } + [BackgroundDependencyLoader] + private void load(OverlayColourProvider colourProvider) + { + text.Colour = darkText ? colourProvider.Background3 : colourProvider.Content1; + } + public LocalisableString TooltipText { get; } } - - private sealed partial class ColouredModSwitchTiny : ModSwitchTiny, IHasCustomTooltip - { - public Mod? TooltipContent { get; } - - [Resolved] - private OverlayColourProvider colourProvider { get; set; } = null!; - - public ColouredModSwitchTiny(Mod mod) - : base(mod) - { - TooltipContent = mod; - Active.Value = true; - } - - public ITooltip GetCustomTooltip() => new ModTooltip(colourProvider); - } - - private sealed partial class MoreModSwitchTiny : CompositeDrawable - { - private readonly int count; - - public MoreModSwitchTiny(int count) - { - this.count = count; - } - - [BackgroundDependencyLoader] - private void load(OsuColour colours) - { - Size = new Vector2(ModSwitchTiny.WIDTH, ModSwitchTiny.DEFAULT_HEIGHT); - - InternalChild = new CircularContainer - { - Masking = true, - RelativeSizeAxes = Axes.Both, - Children = new Drawable[] - { - new Box - { - RelativeSizeAxes = Axes.Both, - Colour = OsuColour.Gray(0.2f), - }, - new OsuSpriteText - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Shadow = false, - Font = OsuFont.Numeric.With(size: 24, weight: FontWeight.Black), - Text = $"+{count}", - Colour = colours.Yellow, - Margin = new MarginPadding - { - Top = 4 - } - } - } - }; - } - } - - #endregion - - public MenuItem[] ContextMenuItems - { - get - { - List items = new List(); - - // system mods should never be copied across regardless of anything. - var copyableMods = score.Mods.Where(m => IsValidMod.Invoke(m) && m.Type != ModType.System).ToArray(); - - if (copyableMods.Length > 0) - items.Add(new OsuMenuItem("Use these mods", MenuItemType.Highlighted, () => SelectedMods.Value = copyableMods)); - - if (score.OnlineID > 0) - items.Add(new OsuMenuItem(CommonStrings.CopyLink, MenuItemType.Standard, () => clipboard?.SetText($@"{api.Endpoints.WebsiteUrl}/scores/{score.OnlineID}"))); - - if (score.Files.Count <= 0) return items.ToArray(); - - items.Add(new OsuMenuItem(CommonStrings.Export, MenuItemType.Standard, () => scoreManager.Export(score))); - items.Add(new OsuMenuItem(Resources.Localisation.Web.CommonStrings.ButtonsDelete, MenuItemType.Destructive, () => dialogOverlay?.Push(new LocalScoreDeleteDialog(score)))); - - return items.ToArray(); - } - } } } diff --git a/osu.Game/Screens/SelectV2/BeatmapLeaderboardScore_Tooltip.cs b/osu.Game/Screens/SelectV2/BeatmapLeaderboardScore_Tooltip.cs new file mode 100644 index 0000000000..c6fe1e5f25 --- /dev/null +++ b/osu.Game/Screens/SelectV2/BeatmapLeaderboardScore_Tooltip.cs @@ -0,0 +1,371 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Linq; +using osu.Framework.Allocation; +using osu.Framework.Extensions.Color4Extensions; +using osu.Framework.Extensions.LocalisationExtensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Colour; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Cursor; +using osu.Framework.Graphics.Effects; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Localisation; +using osu.Game.Graphics; +using osu.Game.Graphics.Containers; +using osu.Game.Graphics.Sprites; +using osu.Game.Online.Leaderboards; +using osu.Game.Overlays; +using osu.Game.Resources.Localisation.Web; +using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.UI; +using osu.Game.Scoring; +using osu.Game.Utils; +using osuTK; +using osuTK.Graphics; + +namespace osu.Game.Screens.SelectV2 +{ + public partial class BeatmapLeaderboardScore + { + public partial class LeaderboardScoreTooltip : VisibilityContainer, ITooltip + { + private const float spacing = 20f; + + private DateAndStatisticsPanel dateAndStatistics = null!; + private ModsPanel modsPanel = null!; + private TotalScoreRankPanel totalScoreRankPanel = null!; + + [Cached] + private readonly OverlayColourProvider colourProvider; + + public LeaderboardScoreTooltip(OverlayColourProvider colourProvider) + { + this.colourProvider = colourProvider; + } + + [BackgroundDependencyLoader] + private void load() + { + Width = 170; + AutoSizeAxes = Axes.Y; + + InternalChild = new ReverseChildIDFillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Spacing = new Vector2(0f, -spacing), + Children = new Drawable[] + { + dateAndStatistics = new DateAndStatisticsPanel(), + modsPanel = new ModsPanel(), + totalScoreRankPanel = new TotalScoreRankPanel(), + }, + }; + } + + private ScoreInfo? lastContent; + + public void SetContent(ScoreInfo content) + { + if (lastContent != null && lastContent.Equals(content)) + return; + + dateAndStatistics.Score = content; + modsPanel.Score = content; + totalScoreRankPanel.Score = content; + lastContent = content; + } + + protected override void PopIn() => this.FadeIn(300, Easing.OutQuint); + protected override void PopOut() => this.FadeOut(300, Easing.OutQuint); + public void Move(Vector2 pos) => Position = pos; + + private partial class DateAndStatisticsPanel : CompositeDrawable + { + private OsuSpriteText absoluteDate = null!; + private DrawableDate relativeDate = null!; + private FillFlowContainer statistics = null!; + + [Resolved] + private OsuColour colours { get; set; } = null!; + + [Resolved] + private OverlayColourProvider colourProvider { get; set; } = null!; + + public ScoreInfo Score + { + set + { + absoluteDate.Text = value.Date.ToLocalisableString(@"dd MMMM yyyy h:mm tt"); + relativeDate.Date = value.Date; + + var judgementsStatistics = value.GetStatisticsForDisplay().Select(s => + new StatisticRow(s.DisplayName.ToUpper(), colours.ForHitResult(s.Result), s.Count.ToLocalisableString("N0"))); + + double multiplier = 1.0; + + foreach (var mod in value.Mods) + multiplier *= mod.ScoreMultiplier; + + var generalStatistics = new[] + { + new StatisticRow("Score Multiplier", colourProvider.Content2, ModUtils.FormatScoreMultiplier(multiplier)), + new StatisticRow(BeatmapsetsStrings.ShowScoreboardHeadersCombo, colourProvider.Content2, value.MaxCombo.ToLocalisableString(@"0\x")), + new StatisticRow(BeatmapsetsStrings.ShowScoreboardHeadersAccuracy, colourProvider.Content2, value.Accuracy.FormatAccuracy()), + }; + + if (value.PP != null) + { + generalStatistics = new[] + { + new StatisticRow(BeatmapsetsStrings.ShowScoreboardHeaderspp.ToUpper(), colourProvider.Content2, value.PP.ToLocalisableString("N0")) + }.Concat(generalStatistics).ToArray(); + } + + statistics.ChildrenEnumerable = judgementsStatistics + .Append(Empty().With(d => d.Height = 20)) + .Concat(generalStatistics); + } + } + + [BackgroundDependencyLoader] + private void load() + { + RelativeSizeAxes = Axes.X; + AutoSizeAxes = Axes.Y; + CornerRadius = corner_radius; + Masking = true; + + EdgeEffect = new EdgeEffectParameters + { + Type = EdgeEffectType.Shadow, + Colour = Color4.Black.Opacity(0.25f), + Radius = 4f, + }; + + InternalChildren = new Drawable[] + { + new Box + { + Colour = colourProvider.Background4, + RelativeSizeAxes = Axes.Both, + }, + new FillFlowContainer + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Vertical, + Spacing = new Vector2(0f, 4f), + Margin = new MarginPadding { Top = 8f }, + Children = new Drawable[] + { + absoluteDate = new OsuSpriteText + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + Font = OsuFont.Style.Caption1.With(weight: FontWeight.SemiBold), + UseFullGlyphHeight = false, + }, + relativeDate = new DrawableDate(default, OsuFont.Style.Caption1.Size) + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + Colour = colourProvider.Content2, + UseFullGlyphHeight = false, + }, + new Container + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + CornerRadius = corner_radius, + Masking = true, + Margin = new MarginPadding { Top = 4f }, + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = colourProvider.Background3, + }, + statistics = new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Spacing = new Vector2(0f, 4f), + Padding = new MarginPadding(8f), + }, + }, + }, + }, + }, + }; + } + } + + private partial class StatisticRow : CompositeDrawable + { + public StatisticRow(LocalisableString label, Color4 labelColour, LocalisableString value) + { + RelativeSizeAxes = Axes.X; + AutoSizeAxes = Axes.Y; + + InternalChildren = new[] + { + new OsuSpriteText + { + Text = label, + Colour = labelColour, + Font = OsuFont.Style.Caption2.With(weight: FontWeight.SemiBold), + }, + new OsuSpriteText + { + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight, + Text = value, + Colour = Color4.White, + Font = OsuFont.Style.Caption2, + }, + }; + } + } + + private partial class ModsPanel : CompositeDrawable + { + private FillFlowContainer modsFlow = null!; + + public ScoreInfo Score + { + set + { + var mods = value.Mods; + + if (!mods.Any()) + Hide(); + else + { + Show(); + + modsFlow.ChildrenEnumerable = mods.AsOrdered().Select(m => new ModIcon(m) + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Scale = new Vector2(0.3f), + }); + } + } + } + + [BackgroundDependencyLoader] + private void load(OverlayColourProvider colourProvider) + { + RelativeSizeAxes = Axes.X; + AutoSizeAxes = Axes.Y; + CornerRadius = corner_radius; + Masking = true; + + EdgeEffect = new EdgeEffectParameters + { + Type = EdgeEffectType.Shadow, + Colour = Color4.Black.Opacity(0.25f), + Radius = 4f, + }; + + InternalChildren = new Drawable[] + { + new Box + { + Colour = colourProvider.Background4, + RelativeSizeAxes = Axes.Both, + }, + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = Color4.Transparent, + }, + modsFlow = new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Margin = new MarginPadding { Bottom = 6f, Top = 6f + spacing }, + Padding = new MarginPadding { Horizontal = 16f }, + Spacing = new Vector2(2f, -4f), + }, + }; + } + } + + public partial class TotalScoreRankPanel : CompositeDrawable + { + private Box rankBackground = null!; + private Container rankContainer = null!; + private OsuSpriteText totalScore = null!; + + [Resolved] + private ScoreManager scoreManager { get; set; } = null!; + + public ScoreInfo Score + { + set + { + rankBackground.Colour = ColourInfo.GradientVertical( + OsuColour.ForRank(value.Rank).Opacity(0f), + OsuColour.ForRank(value.Rank).Opacity(0.5f)); + rankContainer.Child = new DrawableRank(value.Rank); + totalScore.Current = scoreManager.GetBindableTotalScoreString(value); + } + } + + [BackgroundDependencyLoader] + private void load() + { + RelativeSizeAxes = Axes.X; + AutoSizeAxes = Axes.Y; + CornerRadius = corner_radius; + Masking = true; + + EdgeEffect = new EdgeEffectParameters + { + Type = EdgeEffectType.Shadow, + Colour = Color4.Black.Opacity(0.25f), + Radius = 4f, + }; + + InternalChildren = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = Color4Extensions.FromHex("#353535"), + }, + rankBackground = new Box + { + RelativeSizeAxes = Axes.Both, + }, + rankContainer = new Container + { + Anchor = Anchor.BottomCentre, + Origin = Anchor.BottomCentre, + Size = new Vector2(25f, 14f), + Margin = new MarginPadding { Bottom = 5f }, + }, + totalScore = new OsuSpriteText + { + Anchor = Anchor.BottomCentre, + Origin = Anchor.BottomCentre, + Margin = new MarginPadding { Bottom = 25f, Top = 10f + spacing }, + Font = OsuFont.Style.Subtitle.With(weight: FontWeight.Light, fixedWidth: true), + Spacing = new Vector2(-1.5f), + UseFullGlyphHeight = false, + }, + }; + } + } + } + } +} diff --git a/osu.Game/Screens/SelectV2/BeatmapLeaderboardWedge.cs b/osu.Game/Screens/SelectV2/BeatmapLeaderboardWedge.cs new file mode 100644 index 0000000000..b8c4d07d04 --- /dev/null +++ b/osu.Game/Screens/SelectV2/BeatmapLeaderboardWedge.cs @@ -0,0 +1,381 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.Sprites; +using osu.Game.Beatmaps; +using osu.Game.Graphics; +using osu.Game.Graphics.Containers; +using osu.Game.Graphics.Cursor; +using osu.Game.Graphics.Sprites; +using osu.Game.Graphics.UserInterface; +using osu.Game.Localisation; +using osu.Game.Online.Leaderboards; +using osu.Game.Online.Placeholders; +using osu.Game.Overlays; +using osu.Game.Rulesets; +using osu.Game.Rulesets.Mods; +using osu.Game.Scoring; +using osu.Game.Screens.Select.Leaderboards; +using osuTK; + +namespace osu.Game.Screens.SelectV2 +{ + public partial class BeatmapLeaderboardWedge : VisibilityContainer + { + public IBindable Scope { get; } = new Bindable(); + + public IBindable FilterBySelectedMods { get; } = new BindableBool(); + + [Resolved] + private LeaderboardManager leaderboardManager { get; set; } = null!; + + [Resolved] + private IBindable beatmap { get; set; } = null!; + + [Resolved] + private IBindable ruleset { get; set; } = null!; + + [Resolved] + private IBindable> mods { get; set; } = null!; + + [Resolved] + private OverlayColourProvider colourProvider { get; set; } = null!; + + private Container placeholderContainer = null!; + private Placeholder? placeholder; + + private Container scoresContainer = null!; + + private OsuScrollContainer scoresScroll = null!; + private Container personalBestDisplay = null!; + + private Container personalBestScoreContainer = null!; + private LoadingLayer loading = null!; + + private CancellationTokenSource? cancellationTokenSource; + + private readonly IBindable fetchedScores = new Bindable(); + + private const float personal_best_height = 80; + + [BackgroundDependencyLoader] + private void load() + { + RelativeSizeAxes = Axes.Both; + + Child = new OsuContextMenuContainer + { + RelativeSizeAxes = Axes.Both, + Children = new Drawable[] + { + scoresScroll = new OsuScrollContainer + { + RelativeSizeAxes = Axes.Both, + ScrollbarVisible = false, + Shear = OsuGame.SHEAR, + Child = scoresContainer = new Container + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Padding = new MarginPadding + { + Top = 5, + // Left padding offsets the shear to create a visually appealing list display. + Left = 80f, + // Bottom padding ensures the last entry's full width is displayed + // (ie it is fully on screen after shear is considered). + Bottom = BeatmapLeaderboardScore.HEIGHT * 3 + }, + }, + }, + personalBestDisplay = new Container + { + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + RelativeSizeAxes = Axes.X, + Height = personal_best_height, + Shear = OsuGame.SHEAR, + Margin = new MarginPadding { Left = -40f }, + CornerRadius = 10f, + Masking = true, + // push the personal best 1px down to hide masking issues + Y = 1f, + X = -100f, + Alpha = 0f, + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = colourProvider.Background4, + }, + new Container + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Shear = -OsuGame.SHEAR, + Padding = new MarginPadding { Top = 5f, Bottom = 5f, Left = 70f, Right = 10f }, + Children = new Drawable[] + { + new OsuSpriteText + { + Colour = colourProvider.Content2, + Text = "Personal Best", + Font = OsuFont.Style.Caption1.With(weight: FontWeight.SemiBold), + }, + personalBestScoreContainer = new Container + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Margin = new MarginPadding { Top = 20f }, + }, + } + }, + }, + }, + placeholderContainer = new Container + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.Both, + }, + loading = new LoadingLayer(), + } + }; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + Scope.BindValueChanged(_ => refetchScores()); + FilterBySelectedMods.BindValueChanged(_ => refetchScores()); + beatmap.BindValueChanged(_ => refetchScores()); + ruleset.BindValueChanged(_ => refetchScores()); + mods.BindValueChanged(_ => refetchScoresFromMods()); + + refetchScores(); + } + + protected override void PopIn() + { + this.FadeIn(300, Easing.OutQuint); + } + + protected override void PopOut() + { + this.FadeOut(300, Easing.OutQuint); + } + + private void refetchScoresFromMods() + { + if (FilterBySelectedMods.Value) + refetchScores(); + } + + private bool initialFetchComplete; + + private void refetchScores() + { + SetScores(Array.Empty(), null); + + if (beatmap.IsDefault) + { + SetState(LeaderboardState.NoneSelected); + return; + } + + SetState(LeaderboardState.Retrieving); + + var fetchBeatmapInfo = beatmap.Value.BeatmapInfo; + var fetchRuleset = ruleset.Value ?? fetchBeatmapInfo.Ruleset; + + // For now, we forcefully refresh to keep things simple. + // In the future, removing this requirement may be deemed useful, but will need ample testing of edge case scenarios + // (like returning from gameplay after setting a new score, returning to song select after main menu). + leaderboardManager.FetchWithCriteria(new LeaderboardCriteria(fetchBeatmapInfo, fetchRuleset, Scope.Value, FilterBySelectedMods.Value ? mods.Value.ToArray() : null), forceRefresh: true); + + if (!initialFetchComplete) + { + // only bind this after the first fetch to avoid reading stale scores. + fetchedScores.BindTo(leaderboardManager.Scores); + fetchedScores.BindValueChanged(_ => updateScores(), true); + initialFetchComplete = true; + } + } + + private void updateScores() + { + var scores = fetchedScores.Value; + + if (scores == null) return; + + if (scores.FailState != null) + SetState((LeaderboardState)scores.FailState); + else + SetScores(scores.TopScores, scores.UserScore); + } + + protected void SetScores(IEnumerable scores, ScoreInfo? userScore) + { + cancellationTokenSource?.Cancel(); + cancellationTokenSource = new CancellationTokenSource(); + + clearScores(); + SetState(LeaderboardState.Success); + + if (!scores.Any()) + { + SetState(LeaderboardState.NoScores); + return; + } + + LoadComponentsAsync(scores.Select((s, i) => new BeatmapLeaderboardScore(s) + { + Rank = i + 1, + IsPersonalBest = s.OnlineID == userScore?.OnlineID, + SelectedMods = { BindTarget = mods }, + }), loadedScores => + { + int delay = 200; + int i = 0; + + foreach (var d in loadedScores) + { + d.Y = (BeatmapLeaderboardScore.HEIGHT + 4f) * 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). + d.Shear = Vector2.Zero; + + scoresContainer.Add(d); + + d.FadeOut() + .MoveToX(-20f) + .Delay(delay) + .FadeIn(300, Easing.OutQuint) + .MoveToX(0f, 300, Easing.OutQuint); + + delay += 30; + i++; + } + }, cancellation: cancellationTokenSource.Token); + + if (userScore != null) + { + personalBestDisplay.MoveToX(0, 600, Easing.OutQuint); + personalBestDisplay.FadeIn(600, Easing.OutQuint); + personalBestScoreContainer.Child = new BeatmapLeaderboardScore(userScore) + { + IsPersonalBest = true, + Rank = userScore.Position, + SelectedMods = { BindTarget = mods }, + }; + + scoresScroll.TransformTo(nameof(scoresScroll.Padding), new MarginPadding { Bottom = personal_best_height }, 300, Easing.OutQuint); + } + } + + private void clearScores() + { + float delay = 0; + + foreach (var d in scoresContainer) + { + // Avoid applying animations a second time to drawables which are already fading out. + if (d.LifetimeEnd != double.MaxValue) + continue; + + d.Delay(delay) + .MoveToX(-10f, 120, Easing.Out) + .FadeOut(120, Easing.Out) + .Expire(); + + delay += 20; + } + + personalBestDisplay.MoveToX(-100, 300, Easing.OutQuint); + personalBestDisplay.FadeOut(300, Easing.OutQuint); + scoresScroll.TransformTo(nameof(scoresScroll.Padding), new MarginPadding(), 300, Easing.OutQuint); + } + + private LeaderboardState displayedState; + + protected void SetState(LeaderboardState state) + { + if (state == displayedState) + return; + + if (state == LeaderboardState.Retrieving) + loading.Show(); + else + loading.Hide(); + + displayedState = state; + + placeholder?.FadeOut(150, Easing.OutQuint).Expire(); + placeholder = getPlaceholderFor(state); + + if (placeholder == null) + return; + + clearScores(); + + placeholderContainer.Child = placeholder; + + placeholder.ScaleTo(0.8f).Then().ScaleTo(1, 900, Easing.OutQuint); + placeholder.FadeInFromZero(300, Easing.OutQuint); + } + + private Placeholder? getPlaceholderFor(LeaderboardState state) + { + switch (state) + { + case LeaderboardState.NetworkFailure: + return new ClickablePlaceholder(LeaderboardStrings.CouldntFetchScores, FontAwesome.Solid.Sync) + { + Action = refetchScores + }; + + case LeaderboardState.NoneSelected: + return new MessagePlaceholder(LeaderboardStrings.PleaseSelectABeatmap); + + case LeaderboardState.RulesetUnavailable: + return new MessagePlaceholder(LeaderboardStrings.LeaderboardsAreNotAvailableForThisRuleset); + + case LeaderboardState.BeatmapUnavailable: + return new MessagePlaceholder(LeaderboardStrings.LeaderboardsAreNotAvailableForThisBeatmap); + + case LeaderboardState.NoScores: + return new MessagePlaceholder(LeaderboardStrings.NoRecordsYet); + + case LeaderboardState.NotLoggedIn: + return new LoginPlaceholder(LeaderboardStrings.PleaseSignInToViewOnlineLeaderboards); + + case LeaderboardState.NotSupporter: + return new MessagePlaceholder(LeaderboardStrings.PleaseInvestInAnOsuSupporterTagToViewThisLeaderboard); + + case LeaderboardState.NoTeam: + return new MessagePlaceholder(LeaderboardStrings.NoTeam); + + case LeaderboardState.Retrieving: + return null; + + case LeaderboardState.Success: + return null; + + default: + throw new ArgumentOutOfRangeException(nameof(state)); + } + } + } +}