diff --git a/osu.Game.Tests/Visual/SongSelect/BeatmapCarouselV2TestScene.cs b/osu.Game.Tests/Visual/SongSelect/BeatmapCarouselV2TestScene.cs index 570202d4aeb7..2c902a466fea 100644 --- a/osu.Game.Tests/Visual/SongSelect/BeatmapCarouselV2TestScene.cs +++ b/osu.Game.Tests/Visual/SongSelect/BeatmapCarouselV2TestScene.cs @@ -17,6 +17,7 @@ using osu.Game.Database; using osu.Game.Graphics; using osu.Game.Graphics.Containers; +using osu.Game.Overlays; using osu.Game.Screens.Select; using osu.Game.Screens.SelectV2; using osu.Game.Tests.Beatmaps; @@ -39,6 +40,9 @@ public abstract partial class BeatmapCarouselV2TestScene : OsuManualInputManager [Cached(typeof(BeatmapStore))] private BeatmapStore store; + [Cached] + private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Aquamarine); + private OsuTextFlowContainer stats = null!; private int beatmapCount; @@ -94,7 +98,7 @@ protected void CreateCarousel() { Anchor = Anchor.Centre, Origin = Anchor.Centre, - Width = 500, + Width = 800, RelativeSizeAxes = Axes.Y, }, }, @@ -185,6 +189,7 @@ protected void ClickVisiblePanel(int index) .Where(p => ((ICarouselPanel)p).Item?.IsVisible == true) .OrderBy(p => p.Y) .ElementAt(index) + .ChildrenOfType().Single() .TriggerClick(); }); } diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2ArtistGrouping.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2ArtistGrouping.cs index d3eeee151ac9..c378871eac03 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2ArtistGrouping.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2ArtistGrouping.cs @@ -28,42 +28,42 @@ public void SetUpSteps() [Test] public void TestOpenCloseGroupWithNoSelectionMouse() { - AddAssert("no beatmaps visible", () => Carousel.ChildrenOfType().Count(p => p.Alpha > 0), () => Is.Zero); - AddUntilStep("no sets visible", () => Carousel.ChildrenOfType().Count(p => p.Alpha > 0), () => Is.Zero); + AddAssert("no beatmaps visible", () => Carousel.ChildrenOfType().Count(p => p.Alpha > 0), () => Is.Zero); + AddUntilStep("no sets visible", () => Carousel.ChildrenOfType().Count(p => p.Alpha > 0), () => Is.Zero); CheckNoSelection(); - ClickVisiblePanel(0); + ClickVisiblePanel(0); - AddUntilStep("some sets visible", () => Carousel.ChildrenOfType().Count(p => p.Alpha > 0), () => Is.GreaterThan(0)); - AddUntilStep("no beatmaps visible", () => Carousel.ChildrenOfType().Count(p => p.Alpha > 0), () => Is.Zero); + AddUntilStep("some sets visible", () => Carousel.ChildrenOfType().Count(p => p.Alpha > 0), () => Is.GreaterThan(0)); + AddUntilStep("no beatmaps visible", () => Carousel.ChildrenOfType().Count(p => p.Alpha > 0), () => Is.Zero); CheckNoSelection(); - ClickVisiblePanel(0); + ClickVisiblePanel(0); - AddUntilStep("no sets visible", () => Carousel.ChildrenOfType().Count(p => p.Alpha > 0), () => Is.Zero); - AddUntilStep("no beatmaps visible", () => Carousel.ChildrenOfType().Count(p => p.Alpha > 0), () => Is.Zero); + AddUntilStep("no sets visible", () => Carousel.ChildrenOfType().Count(p => p.Alpha > 0), () => Is.Zero); + AddUntilStep("no beatmaps visible", () => Carousel.ChildrenOfType().Count(p => p.Alpha > 0), () => Is.Zero); CheckNoSelection(); } [Test] public void TestOpenCloseGroupWithNoSelectionKeyboard() { - AddAssert("no beatmaps visible", () => Carousel.ChildrenOfType().Count(p => p.Alpha > 0), () => Is.Zero); - AddUntilStep("no sets visible", () => Carousel.ChildrenOfType().Count(p => p.Alpha > 0), () => Is.Zero); + AddAssert("no beatmaps visible", () => Carousel.ChildrenOfType().Count(p => p.Alpha > 0), () => Is.Zero); + AddUntilStep("no sets visible", () => Carousel.ChildrenOfType().Count(p => p.Alpha > 0), () => Is.Zero); CheckNoSelection(); SelectNextPanel(); Select(); - AddUntilStep("some sets visible", () => Carousel.ChildrenOfType().Count(p => p.Alpha > 0), () => Is.GreaterThan(0)); - AddUntilStep("no beatmaps visible", () => Carousel.ChildrenOfType().Count(p => p.Alpha > 0), () => Is.Zero); + AddUntilStep("some sets visible", () => Carousel.ChildrenOfType().Count(p => p.Alpha > 0), () => Is.GreaterThan(0)); + AddUntilStep("no beatmaps visible", () => Carousel.ChildrenOfType().Count(p => p.Alpha > 0), () => Is.Zero); AddAssert("keyboard selected is expanded", () => GetKeyboardSelectedPanel()?.Expanded.Value, () => Is.True); CheckNoSelection(); Select(); - AddUntilStep("no sets visible", () => Carousel.ChildrenOfType().Count(p => p.Alpha > 0), () => Is.Zero); - AddUntilStep("no beatmaps visible", () => Carousel.ChildrenOfType().Count(p => p.Alpha > 0), () => Is.Zero); + AddUntilStep("no sets visible", () => Carousel.ChildrenOfType().Count(p => p.Alpha > 0), () => Is.Zero); + AddUntilStep("no beatmaps visible", () => Carousel.ChildrenOfType().Count(p => p.Alpha > 0), () => Is.Zero); AddAssert("keyboard selected is collapsed", () => GetKeyboardSelectedPanel()?.Expanded.Value, () => Is.False); CheckNoSelection(); } @@ -96,10 +96,10 @@ public void TestCarouselRemembersSelection() AddUntilStep("drawable selection restored", () => GetSelectedPanel()?.Item?.Model, () => Is.EqualTo(selection)); AddAssert("carousel item is visible", () => GetSelectedPanel()?.Item?.IsVisible, () => Is.True); - ClickVisiblePanel(0); + ClickVisiblePanel(0); AddUntilStep("carousel item not visible", GetSelectedPanel, () => Is.Null); - ClickVisiblePanel(0); + ClickVisiblePanel(0); AddUntilStep("carousel item is visible", () => GetSelectedPanel()?.Item?.IsVisible, () => Is.True); } @@ -137,7 +137,7 @@ public void TestKeyboardSelection() // open first group Select(); CheckNoSelection(); - AddUntilStep("some beatmaps visible", () => Carousel.ChildrenOfType().Count(p => p.Alpha > 0), () => Is.GreaterThan(0)); + AddUntilStep("some beatmaps visible", () => Carousel.ChildrenOfType().Count(p => p.Alpha > 0), () => Is.GreaterThan(0)); SelectNextPanel(); Select(); diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2DifficultyGrouping.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2DifficultyGrouping.cs index a0c56020ab6f..239c693ee17c 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2DifficultyGrouping.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2DifficultyGrouping.cs @@ -29,32 +29,32 @@ public void SetUpSteps() [Test] public void TestOpenCloseGroupWithNoSelectionMouse() { - AddAssert("no beatmaps visible", () => Carousel.ChildrenOfType().Count(p => p.Alpha > 0), () => Is.Zero); + AddAssert("no beatmaps visible", () => Carousel.ChildrenOfType().Count(p => p.Alpha > 0), () => Is.Zero); CheckNoSelection(); - ClickVisiblePanel(0); - AddUntilStep("some beatmaps visible", () => Carousel.ChildrenOfType().Count(p => p.Alpha > 0), () => Is.GreaterThan(0)); + ClickVisiblePanel(0); + AddUntilStep("some beatmaps visible", () => Carousel.ChildrenOfType().Count(p => p.Alpha > 0), () => Is.GreaterThan(0)); CheckNoSelection(); - ClickVisiblePanel(0); - AddUntilStep("no beatmaps visible", () => Carousel.ChildrenOfType().Count(p => p.Alpha > 0), () => Is.Zero); + ClickVisiblePanel(0); + AddUntilStep("no beatmaps visible", () => Carousel.ChildrenOfType().Count(p => p.Alpha > 0), () => Is.Zero); CheckNoSelection(); } [Test] public void TestOpenCloseGroupWithNoSelectionKeyboard() { - AddAssert("no beatmaps visible", () => Carousel.ChildrenOfType().Count(p => p.Alpha > 0), () => Is.Zero); + AddAssert("no beatmaps visible", () => Carousel.ChildrenOfType().Count(p => p.Alpha > 0), () => Is.Zero); CheckNoSelection(); SelectNextPanel(); Select(); - AddUntilStep("some beatmaps visible", () => Carousel.ChildrenOfType().Count(p => p.Alpha > 0), () => Is.GreaterThan(0)); + AddUntilStep("some beatmaps visible", () => Carousel.ChildrenOfType().Count(p => p.Alpha > 0), () => Is.GreaterThan(0)); AddAssert("keyboard selected is expanded", () => GetKeyboardSelectedPanel()?.Expanded.Value, () => Is.True); CheckNoSelection(); Select(); - AddUntilStep("no beatmaps visible", () => Carousel.ChildrenOfType().Count(p => p.Alpha > 0), () => Is.Zero); + AddUntilStep("no beatmaps visible", () => Carousel.ChildrenOfType().Count(p => p.Alpha > 0), () => Is.Zero); AddAssert("keyboard selected is collapsed", () => GetKeyboardSelectedPanel()?.Expanded.Value, () => Is.False); CheckNoSelection(); } @@ -87,10 +87,10 @@ public void TestCarouselRemembersSelection() AddUntilStep("drawable selection restored", () => GetSelectedPanel()?.Item?.Model, () => Is.EqualTo(selection)); AddAssert("carousel item is visible", () => GetSelectedPanel()?.Item?.IsVisible, () => Is.True); - ClickVisiblePanel(0); + ClickVisiblePanel(0); AddUntilStep("carousel item not visible", GetSelectedPanel, () => Is.Null); - ClickVisiblePanel(0); + ClickVisiblePanel(0); AddUntilStep("carousel item is visible", () => GetSelectedPanel()?.Item?.IsVisible, () => Is.True); } @@ -120,18 +120,18 @@ public void TestGroupSelectionOnHeaderMouse() SelectNextGroup(); WaitForGroupSelection(0, 0); - AddAssert("keyboard selected panel is beatmap", GetKeyboardSelectedPanel, Is.TypeOf); - AddAssert("selected panel is beatmap", GetSelectedPanel, Is.TypeOf); + AddAssert("keyboard selected panel is beatmap", GetKeyboardSelectedPanel, Is.TypeOf); + AddAssert("selected panel is beatmap", GetSelectedPanel, Is.TypeOf); - ClickVisiblePanel(0); - AddAssert("keyboard selected panel is group", GetKeyboardSelectedPanel, Is.TypeOf); + ClickVisiblePanel(0); + AddAssert("keyboard selected panel is group", GetKeyboardSelectedPanel, Is.TypeOf); AddAssert("keyboard selected panel is contracted", () => GetKeyboardSelectedPanel()?.Expanded.Value, () => Is.False); - ClickVisiblePanel(0); - AddAssert("keyboard selected panel is group", GetKeyboardSelectedPanel, Is.TypeOf); + ClickVisiblePanel(0); + AddAssert("keyboard selected panel is group", GetKeyboardSelectedPanel, Is.TypeOf); AddAssert("keyboard selected panel is expanded", () => GetKeyboardSelectedPanel()?.Expanded.Value, () => Is.True); - AddAssert("selected panel is still beatmap", GetSelectedPanel, Is.TypeOf); + AddAssert("selected panel is still beatmap", GetSelectedPanel, Is.TypeOf); } [Test] @@ -146,7 +146,7 @@ public void TestKeyboardSelection() // open first group Select(); CheckNoSelection(); - AddUntilStep("some beatmaps visible", () => Carousel.ChildrenOfType().Count(p => p.Alpha > 0), () => Is.GreaterThan(0)); + AddUntilStep("some beatmaps visible", () => Carousel.ChildrenOfType().Count(p => p.Alpha > 0), () => Is.GreaterThan(0)); SelectNextPanel(); Select(); @@ -171,23 +171,23 @@ public void TestKeyboardSelection() [Test] public void TestInputHandlingWithinGaps() { - AddAssert("no beatmaps visible", () => !GetVisiblePanels().Any()); + AddAssert("no beatmaps visible", () => !GetVisiblePanels().Any()); // Clicks just above the first group panel should not actuate any action. - ClickVisiblePanelWithOffset(0, new Vector2(0, -(GroupPanel.HEIGHT / 2 + 1))); + ClickVisiblePanelWithOffset(0, new Vector2(0, -(PanelGroup.HEIGHT / 2 + 1))); - AddAssert("no beatmaps visible", () => !GetVisiblePanels().Any()); + AddAssert("no beatmaps visible", () => !GetVisiblePanels().Any()); - ClickVisiblePanelWithOffset(0, new Vector2(0, -(GroupPanel.HEIGHT / 2))); + ClickVisiblePanelWithOffset(0, new Vector2(0, -(PanelGroup.HEIGHT / 2))); - AddUntilStep("wait for beatmaps visible", () => GetVisiblePanels().Any()); + AddUntilStep("wait for beatmaps visible", () => GetVisiblePanels().Any()); CheckNoSelection(); // Beatmap panels expand their selection area to cover holes from spacing. - ClickVisiblePanelWithOffset(0, new Vector2(0, -(CarouselItem.DEFAULT_HEIGHT / 2 + 1))); + ClickVisiblePanelWithOffset(0, new Vector2(0, -(CarouselItem.DEFAULT_HEIGHT / 2 + 1))); WaitForGroupSelection(0, 0); - ClickVisiblePanelWithOffset(1, new Vector2(0, (CarouselItem.DEFAULT_HEIGHT / 2 + 1))); + ClickVisiblePanelWithOffset(1, new Vector2(0, (CarouselItem.DEFAULT_HEIGHT / 2 + 1))); WaitForGroupSelection(0, 1); } } diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2NoGrouping.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2NoGrouping.cs index 09ded342c309..b4048a53551c 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2NoGrouping.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2NoGrouping.cs @@ -213,27 +213,27 @@ public void TestInputHandlingWithinGaps() AddBeatmaps(2, 5); WaitForDrawablePanels(); - AddAssert("no beatmaps visible", () => !GetVisiblePanels().Any()); + AddAssert("no beatmaps visible", () => !GetVisiblePanels().Any()); // Clicks just above the first group panel should not actuate any action. - ClickVisiblePanelWithOffset(0, new Vector2(0, -(BeatmapSetPanel.HEIGHT / 2 + 1))); + ClickVisiblePanelWithOffset(0, new Vector2(0, -(PanelBeatmapSet.HEIGHT / 2 + 1))); - AddAssert("no beatmaps visible", () => !GetVisiblePanels().Any()); + AddAssert("no beatmaps visible", () => !GetVisiblePanels().Any()); - ClickVisiblePanelWithOffset(0, new Vector2(0, -(BeatmapSetPanel.HEIGHT / 2))); + ClickVisiblePanelWithOffset(0, new Vector2(0, -(PanelBeatmapSet.HEIGHT / 2))); - AddUntilStep("wait for beatmaps visible", () => GetVisiblePanels().Any()); + AddUntilStep("wait for beatmaps visible", () => GetVisiblePanels().Any()); WaitForSelection(0, 0); // Beatmap panels expand their selection area to cover holes from spacing. - ClickVisiblePanelWithOffset(1, new Vector2(0, -(CarouselItem.DEFAULT_HEIGHT / 2 + 1))); + ClickVisiblePanelWithOffset(1, new Vector2(0, -(CarouselItem.DEFAULT_HEIGHT / 2 + 1))); WaitForSelection(0, 0); // Panels with higher depth will handle clicks in the gutters for simplicity. - ClickVisiblePanelWithOffset(2, new Vector2(0, (CarouselItem.DEFAULT_HEIGHT / 2 + 1))); + ClickVisiblePanelWithOffset(2, new Vector2(0, (CarouselItem.DEFAULT_HEIGHT / 2 + 1))); WaitForSelection(0, 2); - ClickVisiblePanelWithOffset(3, new Vector2(0, (CarouselItem.DEFAULT_HEIGHT / 2 + 1))); + ClickVisiblePanelWithOffset(3, new Vector2(0, (CarouselItem.DEFAULT_HEIGHT / 2 + 1))); WaitForSelection(0, 3); } diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2Scrolling.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2Scrolling.cs index ee6c11595adf..890e1dd6e3d8 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2Scrolling.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarouselV2Scrolling.cs @@ -30,16 +30,16 @@ public void TestScrollPositionMaintainedOnAddSecondSelected() Quad positionBefore = default; AddStep("select middle beatmap", () => Carousel.CurrentSelection = BeatmapSets.ElementAt(BeatmapSets.Count - 2).Beatmaps.First()); - AddStep("scroll to selected item", () => Scroll.ScrollTo(Scroll.ChildrenOfType().Single(p => p.Selected.Value))); + AddStep("scroll to selected item", () => Scroll.ScrollTo(Scroll.ChildrenOfType().Single(p => p.Selected.Value))); WaitForScrolling(); - AddStep("save selected screen position", () => positionBefore = Carousel.ChildrenOfType().FirstOrDefault(p => p.Selected.Value)!.ScreenSpaceDrawQuad); + AddStep("save selected screen position", () => positionBefore = Carousel.ChildrenOfType().FirstOrDefault(p => p.Selected.Value)!.ScreenSpaceDrawQuad); RemoveFirstBeatmap(); WaitForSorting(); - AddAssert("select screen position unchanged", () => Carousel.ChildrenOfType().Single(p => p.Selected.Value).ScreenSpaceDrawQuad, + AddAssert("select screen position unchanged", () => Carousel.ChildrenOfType().Single(p => p.Selected.Value).ScreenSpaceDrawQuad, () => Is.EqualTo(positionBefore)); } @@ -54,11 +54,11 @@ public void TestScrollPositionMaintainedOnAddLastSelected() WaitForScrolling(); - AddStep("save selected screen position", () => positionBefore = Carousel.ChildrenOfType().FirstOrDefault(p => p.Selected.Value)!.ScreenSpaceDrawQuad); + AddStep("save selected screen position", () => positionBefore = Carousel.ChildrenOfType().FirstOrDefault(p => p.Selected.Value)!.ScreenSpaceDrawQuad); RemoveFirstBeatmap(); WaitForSorting(); - AddAssert("select screen position unchanged", () => Carousel.ChildrenOfType().Single(p => p.Selected.Value).ScreenSpaceDrawQuad, + AddAssert("select screen position unchanged", () => Carousel.ChildrenOfType().Single(p => p.Selected.Value).ScreenSpaceDrawQuad, () => Is.EqualTo(positionBefore)); } } diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselV2DifficultyPanel.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselV2DifficultyPanel.cs new file mode 100644 index 000000000000..1947721d5d04 --- /dev/null +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselV2DifficultyPanel.cs @@ -0,0 +1,105 @@ +// 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 NUnit.Framework; +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Utils; +using osu.Game.Beatmaps; +using osu.Game.Overlays; +using osu.Game.Rulesets.Mania; +using osu.Game.Rulesets.Osu; +using osu.Game.Screens.SelectV2; +using osu.Game.Tests.Resources; +using osu.Game.Tests.Visual.UserInterface; +using osuTK; + +namespace osu.Game.Tests.Visual.SongSelectV2 +{ + public partial class TestSceneBeatmapCarouselV2DifficultyPanel : ThemeComparisonTestScene + { + [Resolved] + private BeatmapManager beatmaps { get; set; } = null!; + + private BeatmapInfo beatmap = null!; + + public TestSceneBeatmapCarouselV2DifficultyPanel() + : base(false) + { + } + + [SetUp] + public void SetUp() => Schedule(() => + { + var beatmapSet = beatmaps.GetAllUsableBeatmapSets().FirstOrDefault(b => b.OnlineID == 241526) + ?? beatmaps.GetAllUsableBeatmapSets().FirstOrDefault(b => !b.Protected) + ?? TestResources.CreateTestBeatmapSetInfo(); + + beatmap = beatmapSet.Beatmaps.First(); + }); + + [Test] + public void TestDisplay() + { + AddStep("display", () => CreateThemedContent(OverlayColourScheme.Aquamarine)); + } + + [Test] + public void TestRandomBeatmap() + { + AddStep("random beatmap", () => + { + var randomSet = beatmaps.GetAllUsableBeatmapSets().MinBy(_ => RNG.Next()); + randomSet ??= TestResources.CreateTestBeatmapSetInfo(); + beatmap = randomSet.Beatmaps.MinBy(_ => RNG.Next())!; + + CreateThemedContent(OverlayColourScheme.Aquamarine); + }); + } + + [Test] + public void TestManiaRuleset() + { + AddToggleStep("mania ruleset", v => Ruleset.Value = v ? new ManiaRuleset().RulesetInfo : new OsuRuleset().RulesetInfo); + } + + protected override Drawable CreateContent() + { + return new FillFlowContainer + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Width = 0.5f, + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Vertical, + Spacing = new Vector2(0f, 5f), + Children = new Drawable[] + { + new PanelBeatmap + { + Item = new CarouselItem(beatmap) + }, + new PanelBeatmap + { + Item = new CarouselItem(beatmap), + KeyboardSelected = { Value = true } + }, + new PanelBeatmap + { + Item = new CarouselItem(beatmap), + Selected = { Value = true } + }, + new PanelBeatmap + { + Item = new CarouselItem(beatmap), + KeyboardSelected = { Value = true }, + Selected = { Value = true } + }, + } + }; + } + } +} diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselV2GroupPanel.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselV2GroupPanel.cs new file mode 100644 index 000000000000..9b07f01e5284 --- /dev/null +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselV2GroupPanel.cs @@ -0,0 +1,83 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Game.Screens.SelectV2; +using osu.Game.Tests.Visual.UserInterface; +using osuTK; + +namespace osu.Game.Tests.Visual.SongSelectV2 +{ + public partial class TestSceneBeatmapCarouselV2GroupPanel : ThemeComparisonTestScene + { + public TestSceneBeatmapCarouselV2GroupPanel() + : base(false) + { + } + + protected override Drawable CreateContent() + { + return new FillFlowContainer + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Width = 0.5f, + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Vertical, + Spacing = new Vector2(0f, 5f), + Children = new Drawable[] + { + new PanelGroup + { + Item = new CarouselItem(new GroupDefinition('A', "Group A")) + }, + new PanelGroup + { + Item = new CarouselItem(new GroupDefinition('A', "Group A")), + KeyboardSelected = { Value = true } + }, + new PanelGroup + { + Item = new CarouselItem(new GroupDefinition('A', "Group A")), + Expanded = { Value = true } + }, + new PanelGroup + { + Item = new CarouselItem(new GroupDefinition('A', "Group A")), + KeyboardSelected = { Value = true }, + Expanded = { Value = true } + }, + new PanelGroupStarDifficulty + { + Item = new CarouselItem(new GroupDefinition(1, "1")) + }, + new PanelGroupStarDifficulty + { + Item = new CarouselItem(new GroupDefinition(3, "3")), + Expanded = { Value = true } + }, + new PanelGroupStarDifficulty + { + Item = new CarouselItem(new GroupDefinition(5, "5")), + }, + new PanelGroupStarDifficulty + { + Item = new CarouselItem(new GroupDefinition(7, "7")), + Expanded = { Value = true } + }, + new PanelGroupStarDifficulty + { + Item = new CarouselItem(new GroupDefinition(8, "8")), + }, + new PanelGroupStarDifficulty + { + Item = new CarouselItem(new GroupDefinition(9, "9")), + Expanded = { Value = true } + }, + } + }; + } + } +} diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselV2SetPanel.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselV2SetPanel.cs new file mode 100644 index 000000000000..ef34394e1214 --- /dev/null +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselV2SetPanel.cs @@ -0,0 +1,95 @@ +// 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 NUnit.Framework; +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Utils; +using osu.Game.Beatmaps; +using osu.Game.Overlays; +using osu.Game.Screens.SelectV2; +using osu.Game.Tests.Resources; +using osu.Game.Tests.Visual.UserInterface; +using osuTK; + +namespace osu.Game.Tests.Visual.SongSelectV2 +{ + public partial class TestSceneBeatmapCarouselV2SetPanel : ThemeComparisonTestScene + { + [Resolved] + private BeatmapManager beatmaps { get; set; } = null!; + + private BeatmapSetInfo beatmapSet = null!; + + public TestSceneBeatmapCarouselV2SetPanel() + : base(false) + { + } + + [SetUp] + public void SetUp() => Schedule(() => + { + beatmapSet = beatmaps.GetAllUsableBeatmapSets().FirstOrDefault(b => b.OnlineID == 241526) + ?? beatmaps.GetAllUsableBeatmapSets().FirstOrDefault(b => !b.Protected) + ?? TestResources.CreateTestBeatmapSetInfo(); + }); + + [Test] + public void TestDisplay() + { + AddStep("display", () => CreateThemedContent(OverlayColourScheme.Aquamarine)); + } + + [Test] + public void TestRandomBeatmap() + { + AddStep("random beatmap", () => + { + var randomSet = beatmaps.GetAllUsableBeatmapSets().MinBy(_ => RNG.Next()); + randomSet ??= TestResources.CreateTestBeatmapSetInfo(); + beatmapSet = randomSet; + + CreateThemedContent(OverlayColourScheme.Aquamarine); + }); + } + + protected override Drawable CreateContent() + { + return new FillFlowContainer + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Width = 0.5f, + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Vertical, + Spacing = new Vector2(0f, 5f), + Children = new Drawable[] + { + new PanelBeatmapSet + { + Item = new CarouselItem(beatmapSet) + }, + new PanelBeatmapSet + { + Item = new CarouselItem(beatmapSet), + KeyboardSelected = { Value = true } + }, + new PanelBeatmapSet + { + Item = new CarouselItem(beatmapSet), + Expanded = { Value = true } + }, + new PanelBeatmapSet + { + Item = new CarouselItem(beatmapSet), + KeyboardSelected = { Value = true }, + Expanded = { Value = true } + }, + } + }; + } + } +} diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselV2StandalonePanel.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselV2StandalonePanel.cs new file mode 100644 index 000000000000..2dbe9e6cd177 --- /dev/null +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselV2StandalonePanel.cs @@ -0,0 +1,105 @@ +// 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 NUnit.Framework; +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Utils; +using osu.Game.Beatmaps; +using osu.Game.Overlays; +using osu.Game.Rulesets.Mania; +using osu.Game.Rulesets.Osu; +using osu.Game.Screens.SelectV2; +using osu.Game.Tests.Resources; +using osu.Game.Tests.Visual.UserInterface; +using osuTK; + +namespace osu.Game.Tests.Visual.SongSelectV2 +{ + public partial class TestSceneBeatmapCarouselV2StandalonePanel : ThemeComparisonTestScene + { + [Resolved] + private BeatmapManager beatmaps { get; set; } = null!; + + private BeatmapInfo beatmap = null!; + + public TestSceneBeatmapCarouselV2StandalonePanel() + : base(false) + { + } + + [SetUp] + public void SetUp() => Schedule(() => + { + var beatmapSet = beatmaps.GetAllUsableBeatmapSets().FirstOrDefault(b => b.OnlineID == 241526) + ?? beatmaps.GetAllUsableBeatmapSets().FirstOrDefault(b => !b.Protected) + ?? TestResources.CreateTestBeatmapSetInfo(); + + beatmap = beatmapSet.Beatmaps.First(); + }); + + [Test] + public void TestDisplay() + { + AddStep("display", () => CreateThemedContent(OverlayColourScheme.Aquamarine)); + } + + [Test] + public void TestRandomBeatmap() + { + AddStep("random beatmap", () => + { + var randomSet = beatmaps.GetAllUsableBeatmapSets().MinBy(_ => RNG.Next()); + randomSet ??= TestResources.CreateTestBeatmapSetInfo(); + beatmap = randomSet.Beatmaps.MinBy(_ => RNG.Next())!; + + CreateThemedContent(OverlayColourScheme.Aquamarine); + }); + } + + [Test] + public void TestManiaRuleset() + { + AddToggleStep("mania ruleset", v => Ruleset.Value = v ? new ManiaRuleset().RulesetInfo : new OsuRuleset().RulesetInfo); + } + + protected override Drawable CreateContent() + { + return new FillFlowContainer + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Width = 0.5f, + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Vertical, + Spacing = new Vector2(0f, 5f), + Children = new Drawable[] + { + new PanelBeatmapStandalone + { + Item = new CarouselItem(beatmap) + }, + new PanelBeatmapStandalone + { + Item = new CarouselItem(beatmap), + KeyboardSelected = { Value = true } + }, + new PanelBeatmapStandalone + { + Item = new CarouselItem(beatmap), + Selected = { Value = true } + }, + new PanelBeatmapStandalone + { + Item = new CarouselItem(beatmap), + KeyboardSelected = { Value = true }, + Selected = { Value = true } + }, + } + }; + } + } +} diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneSongSelect.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneSongSelect.cs index d43026c9609d..33474d74495b 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestSceneSongSelect.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneSongSelect.cs @@ -9,15 +9,27 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Cursor; +using osu.Framework.Platform; using osu.Framework.Screens; using osu.Framework.Testing; +using osu.Game.Beatmaps; +using osu.Game.Configuration; +using osu.Game.Database; +using osu.Game.Online.API; +using osu.Game.Overlays; using osu.Game.Overlays.Mods; +using osu.Game.Rulesets; +using osu.Game.Rulesets.Catch; +using osu.Game.Rulesets.Mania; using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu.Mods; +using osu.Game.Rulesets.Taiko; using osu.Game.Screens; using osu.Game.Screens.Footer; using osu.Game.Screens.Menu; using osu.Game.Screens.SelectV2.Footer; +using osu.Game.Tests.Resources; using osuTK.Input; namespace osu.Game.Tests.Visual.SongSelectV2 @@ -30,6 +42,10 @@ public partial class TestSceneSongSelect : ScreenTestScene [Cached] private readonly OsuLogo logo; + private BeatmapManager beatmapManager = null!; + + protected override bool UseOnlineAPI => true; + public TestSceneSongSelect() { Children = new Drawable[] @@ -49,6 +65,35 @@ public TestSceneSongSelect() }; } + [BackgroundDependencyLoader] + private void load(GameHost host, IAPIProvider onlineAPI) + { + BeatmapStore beatmapStore; + BeatmapUpdater beatmapUpdater; + BeatmapDifficultyCache difficultyCache; + + // These DI caches are required to ensure for interactive runs this test scene doesn't nuke all user beatmaps in the local install. + // At a point we have isolated interactive test runs enough, this can likely be removed. + Dependencies.Cache(new RealmRulesetStore(Realm)); + Dependencies.Cache(Realm); + Dependencies.Cache(difficultyCache = new BeatmapDifficultyCache()); + Dependencies.Cache(beatmapManager = new BeatmapManager(LocalStorage, Realm, onlineAPI, Audio, Resources, host, Beatmap.Default, difficultyCache)); + Dependencies.CacheAs(beatmapUpdater = new BeatmapUpdater(beatmapManager, difficultyCache, onlineAPI, LocalStorage)); + Dependencies.CacheAs(beatmapStore = new RealmDetachedBeatmapStore()); + + beatmapManager.ProcessBeatmap = (set, scope) => beatmapUpdater.Process(set, scope); + + MusicController music; + Dependencies.Cache(music = new MusicController()); + + // required to get bindables attached + Add(difficultyCache); + Add(music); + Add(beatmapStore); + + Dependencies.Cache(new OsuConfigManager(LocalStorage)); + } + protected override void LoadComplete() { base.LoadComplete(); @@ -64,6 +109,16 @@ public override void SetUpSteps() AddStep("load screen", () => Stack.Push(new Screens.SelectV2.SongSelectV2())); AddUntilStep("wait for load", () => Stack.CurrentScreen is Screens.SelectV2.SongSelectV2 songSelect && songSelect.IsLoaded); + AddStep("import test beatmap", () => beatmapManager.Import(TestResources.GetTestBeatmapForImport())); + } + + [Test] + public void TestRulesets() + { + AddStep("set osu ruleset", () => Ruleset.Value = new OsuRuleset().RulesetInfo); + AddStep("set taiko ruleset", () => Ruleset.Value = new TaikoRuleset().RulesetInfo); + AddStep("set catch ruleset", () => Ruleset.Value = new CatchRuleset().RulesetInfo); + AddStep("set mania ruleset", () => Ruleset.Value = new ManiaRuleset().RulesetInfo); } #region Footer @@ -80,8 +135,11 @@ public void TestMods() AddStep("modified", () => SelectedMods.Value = new List { new OsuModDoubleTime { SpeedChange = { Value = 1.2 } } }); AddStep("modified + one", () => SelectedMods.Value = new List { new OsuModHidden(), new OsuModDoubleTime { SpeedChange = { Value = 1.2 } } }); AddStep("modified + two", () => SelectedMods.Value = new List { new OsuModHidden(), new OsuModHardRock(), new OsuModDoubleTime { SpeedChange = { Value = 1.2 } } }); - AddStep("modified + three", () => SelectedMods.Value = new List { new OsuModHidden(), new OsuModHardRock(), new OsuModClassic(), new OsuModDoubleTime { SpeedChange = { Value = 1.2 } } }); - AddStep("modified + four", () => SelectedMods.Value = new List { new OsuModHidden(), new OsuModHardRock(), new OsuModClassic(), new OsuModDifficultyAdjust(), new OsuModDoubleTime { SpeedChange = { Value = 1.2 } } }); + AddStep("modified + three", + () => SelectedMods.Value = new List { new OsuModHidden(), new OsuModHardRock(), new OsuModClassic(), new OsuModDoubleTime { SpeedChange = { Value = 1.2 } } }); + AddStep("modified + four", + () => SelectedMods.Value = new List + { new OsuModHidden(), new OsuModHardRock(), new OsuModClassic(), new OsuModDifficultyAdjust(), new OsuModDoubleTime { SpeedChange = { Value = 1.2 } } }); AddStep("clear mods", () => SelectedMods.Value = Array.Empty()); AddWaitStep("wait", 3); diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneUpdateBeatmapSetButtonV2.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneUpdateBeatmapSetButtonV2.cs new file mode 100644 index 000000000000..ba3f2635b051 --- /dev/null +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneUpdateBeatmapSetButtonV2.cs @@ -0,0 +1,62 @@ +// 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 NUnit.Framework; +using osu.Framework.Graphics; +using osu.Game.Beatmaps; +using osu.Game.Screens.SelectV2; + +namespace osu.Game.Tests.Visual.SongSelectV2 +{ + public partial class TestSceneUpdateBeatmapSetButtonV2 : OsuTestScene + { + private UpdateBeatmapSetButton button = null!; + + [SetUp] + public void SetUp() => Schedule(() => + { + Child = button = new UpdateBeatmapSetButton + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + }; + }); + + [Test] + public void TestNullBeatmap() + { + AddStep("null beatmap", () => button.BeatmapSet = null); + AddAssert("button invisible", () => button.Alpha == 0f); + } + + [Test] + public void TestUpdatedBeatmap() + { + AddStep("updated beatmap", () => button.BeatmapSet = new BeatmapSetInfo + { + Beatmaps = { new BeatmapInfo() } + }); + AddAssert("button invisible", () => button.Alpha == 0f); + } + + [Test] + public void TestNonUpdatedBeatmap() + { + AddStep("non-updated beatmap", () => button.BeatmapSet = new BeatmapSetInfo + { + Beatmaps = + { + new BeatmapInfo + { + MD5Hash = "test", + OnlineMD5Hash = "online", + LastOnlineUpdate = DateTimeOffset.Now, + } + } + }); + + AddAssert("button visible", () => button.Alpha == 1f); + } + } +} diff --git a/osu.Game/Beatmaps/Drawables/DifficultySpectrumDisplay.cs b/osu.Game/Beatmaps/Drawables/DifficultySpectrumDisplay.cs index 2fb3a8eee4f1..56f6c77ba8dd 100644 --- a/osu.Game/Beatmaps/Drawables/DifficultySpectrumDisplay.cs +++ b/osu.Game/Beatmaps/Drawables/DifficultySpectrumDisplay.cs @@ -28,7 +28,7 @@ public Vector2 DotSize dotSize = value; if (IsLoaded) - updateDotDimensions(); + updateDisplay(); } } @@ -42,13 +42,27 @@ public float DotSpacing dotSpacing = value; if (IsLoaded) - updateDotDimensions(); + updateDisplay(); + } + } + + private IBeatmapSetInfo? beatmapSet; + + public IBeatmapSetInfo? BeatmapSet + { + get => beatmapSet; + set + { + beatmapSet = value; + + if (IsLoaded) + updateDisplay(); } } private readonly FillFlowContainer flow; - public DifficultySpectrumDisplay(IBeatmapSetInfo beatmapSet) + public DifficultySpectrumDisplay(IBeatmapSetInfo? beatmapSet = null) { AutoSizeAxes = Axes.Both; @@ -59,25 +73,31 @@ public DifficultySpectrumDisplay(IBeatmapSetInfo beatmapSet) Direction = FillDirection.Horizontal, }; - // matching web: https://github.com/ppy/osu-web/blob/d06d8c5e735eb1f48799b1654b528e9a7afb0a35/resources/assets/lib/beatmapset-panel.tsx#L127 - bool collapsed = beatmapSet.Beatmaps.Count() > 12; - - foreach (var rulesetGrouping in beatmapSet.Beatmaps.GroupBy(beatmap => beatmap.Ruleset).OrderBy(group => group.Key)) - flow.Add(new RulesetDifficultyGroup(rulesetGrouping.Key.OnlineID, rulesetGrouping, collapsed)); + BeatmapSet = beatmapSet; } protected override void LoadComplete() { base.LoadComplete(); - updateDotDimensions(); + updateDisplay(); } - private void updateDotDimensions() + private void updateDisplay() { - foreach (var group in flow) + flow.Clear(); + + if (beatmapSet == null) + return; + + // matching web: https://github.com/ppy/osu-web/blob/d06d8c5e735eb1f48799b1654b528e9a7afb0a35/resources/assets/lib/beatmapset-panel.tsx#L127 + bool collapsed = beatmapSet.Beatmaps.Count() > 12; + + foreach (var rulesetGrouping in beatmapSet.Beatmaps.GroupBy(beatmap => beatmap.Ruleset).OrderBy(group => group.Key)) { - group.DotSize = DotSize; - group.DotSpacing = DotSpacing; + flow.Add(new RulesetDifficultyGroup(rulesetGrouping.Key.OnlineID, rulesetGrouping, collapsed, dotSize) + { + Spacing = new Vector2(DotSpacing, 0f), + }); } } @@ -86,26 +106,14 @@ private partial class RulesetDifficultyGroup : FillFlowContainer private readonly int rulesetId; private readonly IEnumerable beatmapInfos; private readonly bool collapsed; + private readonly Vector2 dotSize; - public RulesetDifficultyGroup(int rulesetId, IEnumerable beatmapInfos, bool collapsed) + public RulesetDifficultyGroup(int rulesetId, IEnumerable beatmapInfos, bool collapsed, Vector2 dotSize) { this.rulesetId = rulesetId; this.beatmapInfos = beatmapInfos; this.collapsed = collapsed; - } - - public Vector2 DotSize - { - set - { - foreach (var dot in Children.OfType()) - dot.Size = value; - } - } - - public float DotSpacing - { - set => Spacing = new Vector2(value, 0); + this.dotSize = dotSize; } [BackgroundDependencyLoader] @@ -125,7 +133,7 @@ private void load(RulesetStore rulesets) if (!collapsed) { foreach (var beatmapInfo in beatmapInfos.OrderBy(bi => bi.StarRating)) - Add(new DifficultyDot(beatmapInfo.StarRating)); + Add(new DifficultyDot(beatmapInfo.StarRating, dotSize)); } else { @@ -145,9 +153,10 @@ private partial class DifficultyDot : CircularContainer { private readonly double starDifficulty; - public DifficultyDot(double starDifficulty) + public DifficultyDot(double starDifficulty, Vector2 dotSize) { this.starDifficulty = starDifficulty; + Size = dotSize; } [BackgroundDependencyLoader] diff --git a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs index 5ae227f86ce0..c6bce228dcf6 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs @@ -267,9 +267,9 @@ public void Filter(FilterCriteria criteria) #region Drawable pooling - private readonly DrawablePool beatmapPanelPool = new DrawablePool(100); - private readonly DrawablePool setPanelPool = new DrawablePool(100); - private readonly DrawablePool groupPanelPool = new DrawablePool(100); + private readonly DrawablePool beatmapPanelPool = new DrawablePool(100); + private readonly DrawablePool setPanelPool = new DrawablePool(100); + private readonly DrawablePool groupPanelPool = new DrawablePool(100); private void setupPools() { diff --git a/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs b/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs index cb5a40918cbe..8f9d5cc31b6d 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarouselFilterGrouping.cs @@ -70,7 +70,7 @@ public async Task> Run(IEnumerable items addItem(new CarouselItem(newGroup) { - DrawHeight = GroupPanel.HEIGHT, + DrawHeight = PanelGroup.HEIGHT, DepthLayer = -2, }); } @@ -85,7 +85,7 @@ public async Task> Run(IEnumerable items addItem(new CarouselItem(beatmap.BeatmapSet!) { - DrawHeight = BeatmapSetPanel.HEIGHT, + DrawHeight = PanelBeatmapSet.HEIGHT, DepthLayer = -1 }); } diff --git a/osu.Game/Screens/SelectV2/BeatmapPanel.cs b/osu.Game/Screens/SelectV2/BeatmapPanel.cs deleted file mode 100644 index ddf2fdcb57d3..000000000000 --- a/osu.Game/Screens/SelectV2/BeatmapPanel.cs +++ /dev/null @@ -1,119 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using System.Diagnostics; -using osu.Framework.Allocation; -using osu.Framework.Bindables; -using osu.Framework.Extensions.Color4Extensions; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Pooling; -using osu.Framework.Graphics.Shapes; -using osu.Framework.Input.Events; -using osu.Game.Beatmaps; -using osu.Game.Graphics.Sprites; -using osuTK; -using osuTK.Graphics; - -namespace osu.Game.Screens.SelectV2 -{ - public partial class BeatmapPanel : PoolableDrawable, ICarouselPanel - { - [Resolved] - private BeatmapCarousel carousel { get; set; } = null!; - - private Box activationFlash = null!; - private OsuSpriteText text = null!; - - public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) - { - var inputRectangle = DrawRectangle; - - // 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 }); - - return inputRectangle.Contains(ToLocalSpace(screenSpacePos)); - } - - [BackgroundDependencyLoader] - private void load() - { - Size = new Vector2(500, CarouselItem.DEFAULT_HEIGHT); - Masking = true; - - InternalChildren = new Drawable[] - { - new Box - { - Colour = Color4.Aqua.Darken(5), - Alpha = 0.8f, - RelativeSizeAxes = Axes.Both, - }, - activationFlash = new Box - { - Colour = Color4.White, - Blending = BlendingParameters.Additive, - Alpha = 0, - RelativeSizeAxes = Axes.Both, - }, - text = new OsuSpriteText - { - Padding = new MarginPadding(5), - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - } - }; - - Selected.BindValueChanged(value => - { - activationFlash.FadeTo(value.NewValue ? 0.2f : 0, 500, Easing.OutQuint); - }); - - KeyboardSelected.BindValueChanged(value => - { - if (value.NewValue) - { - BorderThickness = 5; - BorderColour = Color4.Pink; - } - else - { - BorderThickness = 0; - } - }); - } - - protected override void PrepareForUse() - { - base.PrepareForUse(); - - Debug.Assert(Item != null); - var beatmap = (BeatmapInfo)Item.Model; - - text.Text = $"Difficulty: {beatmap.DifficultyName} ({beatmap.StarRating:N1}*)"; - - this.FadeInFromZero(500, Easing.OutQuint); - } - - protected override bool OnClick(ClickEvent e) - { - carousel.Activate(Item!); - return true; - } - - #region ICarouselPanel - - public CarouselItem? Item { get; set; } - public BindableBool Selected { get; } = new BindableBool(); - public BindableBool Expanded { get; } = new BindableBool(); - public BindableBool KeyboardSelected { get; } = new BindableBool(); - - public double DrawYPosition { get; set; } - - public void Activated() => activationFlash.FadeOutFromOne(500, Easing.OutQuint); - - #endregion - } -} diff --git a/osu.Game/Screens/SelectV2/BeatmapSetPanel.cs b/osu.Game/Screens/SelectV2/BeatmapSetPanel.cs deleted file mode 100644 index f6c9324077b2..000000000000 --- a/osu.Game/Screens/SelectV2/BeatmapSetPanel.cs +++ /dev/null @@ -1,104 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using System.Diagnostics; -using osu.Framework.Allocation; -using osu.Framework.Bindables; -using osu.Framework.Extensions.Color4Extensions; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Pooling; -using osu.Framework.Graphics.Shapes; -using osu.Framework.Input.Events; -using osu.Game.Beatmaps; -using osu.Game.Graphics.Sprites; -using osuTK; -using osuTK.Graphics; - -namespace osu.Game.Screens.SelectV2 -{ - public partial class BeatmapSetPanel : PoolableDrawable, ICarouselPanel - { - public const float HEIGHT = CarouselItem.DEFAULT_HEIGHT * 2; - - [Resolved] - private BeatmapCarousel carousel { get; set; } = null!; - - private OsuSpriteText text = null!; - private Box box = null!; - - [BackgroundDependencyLoader] - private void load() - { - Size = new Vector2(500, HEIGHT); - Masking = true; - - InternalChildren = new Drawable[] - { - box = new Box - { - Colour = Color4.Yellow.Darken(5), - Alpha = 0.8f, - RelativeSizeAxes = Axes.Both, - }, - text = new OsuSpriteText - { - Padding = new MarginPadding(5), - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - } - }; - - Expanded.BindValueChanged(value => - { - box.FadeColour(value.NewValue ? Color4.Yellow.Darken(2) : Color4.Yellow.Darken(5), 500, Easing.OutQuint); - }); - - KeyboardSelected.BindValueChanged(value => - { - if (value.NewValue) - { - BorderThickness = 5; - BorderColour = Color4.Pink; - } - else - { - BorderThickness = 0; - } - }); - } - - protected override void PrepareForUse() - { - base.PrepareForUse(); - - Debug.Assert(Item != null); - - var beatmapSetInfo = (BeatmapSetInfo)Item.Model; - - text.Text = $"{beatmapSetInfo.Metadata}"; - - this.FadeInFromZero(500, Easing.OutQuint); - } - - protected override bool OnClick(ClickEvent e) - { - carousel.Activate(Item!); - return true; - } - - #region ICarouselPanel - - public CarouselItem? Item { get; set; } - public BindableBool Selected { get; } = new BindableBool(); - public BindableBool Expanded { get; } = new BindableBool(); - public BindableBool KeyboardSelected { get; } = new BindableBool(); - - public double DrawYPosition { get; set; } - - public void Activated() - { - } - - #endregion - } -} diff --git a/osu.Game/Screens/SelectV2/BeatmapSetPanelBackground.cs b/osu.Game/Screens/SelectV2/BeatmapSetPanelBackground.cs new file mode 100644 index 000000000000..435a0ad262e4 --- /dev/null +++ b/osu.Game/Screens/SelectV2/BeatmapSetPanelBackground.cs @@ -0,0 +1,108 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Allocation; +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.Overlays; +using osuTK; +using osuTK.Graphics; + +namespace osu.Game.Screens.SelectV2 +{ + public partial class BeatmapSetPanelBackground : ModelBackedDrawable + { + protected override bool TransformImmediately => true; + + public WorkingBeatmap? Beatmap + { + get => Model; + set => Model = value; + } + + protected override Drawable CreateDrawable(WorkingBeatmap? model) => new BackgroundSprite(model); + + private partial class BackgroundSprite : CompositeDrawable + { + private readonly WorkingBeatmap? working; + + public BackgroundSprite(WorkingBeatmap? working) + { + this.working = working; + + RelativeSizeAxes = Axes.Both; + } + + [BackgroundDependencyLoader] + private void load(OverlayColourProvider colourProvider) + { + var texture = working?.GetPanelBackground(); + + if (texture != null) + { + InternalChildren = new Drawable[] + { + new Sprite + { + RelativeSizeAxes = Axes.Both, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + FillMode = FillMode.Fill, + Texture = texture, + }, + new FillFlowContainer + { + Depth = -1, + RelativeSizeAxes = Axes.Both, + 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, + 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)), + 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, + }, + } + }, + }; + } + else + { + InternalChild = new Box + { + RelativeSizeAxes = Axes.Both, + Colour = colourProvider.Background6, + }; + } + } + } + } +} diff --git a/osu.Game/Screens/SelectV2/CarouselItem.cs b/osu.Game/Screens/SelectV2/CarouselItem.cs index 0ac81800283c..36dc48a4970b 100644 --- a/osu.Game/Screens/SelectV2/CarouselItem.cs +++ b/osu.Game/Screens/SelectV2/CarouselItem.cs @@ -11,7 +11,7 @@ namespace osu.Game.Screens.SelectV2 /// public sealed class CarouselItem : IComparable { - public const float DEFAULT_HEIGHT = 40; + public const float DEFAULT_HEIGHT = 50; /// /// The model this item is representing. diff --git a/osu.Game/Screens/SelectV2/GroupPanel.cs b/osu.Game/Screens/SelectV2/GroupPanel.cs deleted file mode 100644 index e10521f63ea6..000000000000 --- a/osu.Game/Screens/SelectV2/GroupPanel.cs +++ /dev/null @@ -1,117 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using System.Diagnostics; -using osu.Framework.Allocation; -using osu.Framework.Bindables; -using osu.Framework.Extensions.Color4Extensions; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Pooling; -using osu.Framework.Graphics.Shapes; -using osu.Framework.Input.Events; -using osu.Game.Graphics.Sprites; -using osuTK; -using osuTK.Graphics; - -namespace osu.Game.Screens.SelectV2 -{ - public partial class GroupPanel : PoolableDrawable, ICarouselPanel - { - public const float HEIGHT = CarouselItem.DEFAULT_HEIGHT * 2; - - [Resolved] - private BeatmapCarousel carousel { get; set; } = null!; - - private Box activationFlash = null!; - private OsuSpriteText text = null!; - - private Box box = null!; - - [BackgroundDependencyLoader] - private void load() - { - Size = new Vector2(500, HEIGHT); - Masking = true; - - InternalChildren = new Drawable[] - { - box = new Box - { - Colour = Color4.DarkBlue.Darken(5), - Alpha = 0.8f, - RelativeSizeAxes = Axes.Both, - }, - activationFlash = new Box - { - Colour = Color4.White, - Blending = BlendingParameters.Additive, - Alpha = 0, - RelativeSizeAxes = Axes.Both, - }, - text = new OsuSpriteText - { - Padding = new MarginPadding(5), - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - } - }; - - Selected.BindValueChanged(value => - { - activationFlash.FadeTo(value.NewValue ? 0.2f : 0, 500, Easing.OutQuint); - }); - - Expanded.BindValueChanged(value => - { - box.FadeColour(value.NewValue ? Color4.SkyBlue : Color4.DarkBlue.Darken(5), 500, Easing.OutQuint); - }); - - KeyboardSelected.BindValueChanged(value => - { - if (value.NewValue) - { - BorderThickness = 5; - BorderColour = Color4.Pink; - } - else - { - BorderThickness = 0; - } - }); - } - - protected override void PrepareForUse() - { - base.PrepareForUse(); - - Debug.Assert(Item != null); - - GroupDefinition group = (GroupDefinition)Item.Model; - - text.Text = group.Title; - - this.FadeInFromZero(500, Easing.OutQuint); - } - - protected override bool OnClick(ClickEvent e) - { - carousel.Activate(Item!); - return true; - } - - #region ICarouselPanel - - public CarouselItem? Item { get; set; } - public BindableBool Selected { get; } = new BindableBool(); - public BindableBool Expanded { get; } = new BindableBool(); - public BindableBool KeyboardSelected { get; } = new BindableBool(); - - public double DrawYPosition { get; set; } - - public void Activated() - { - } - - #endregion - } -} diff --git a/osu.Game/Screens/SelectV2/PanelBase.cs b/osu.Game/Screens/SelectV2/PanelBase.cs new file mode 100644 index 000000000000..805cbac8eb53 --- /dev/null +++ b/osu.Game/Screens/SelectV2/PanelBase.cs @@ -0,0 +1,258 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +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.Effects; +using osu.Framework.Graphics.Pooling; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Input.Events; +using osu.Game.Graphics; +using osu.Game.Graphics.UserInterface; +using osu.Game.Overlays; +using osuTK; +using osuTK.Graphics; + +namespace osu.Game.Screens.SelectV2 +{ + public abstract partial class PanelBase : PoolableDrawable, ICarouselPanel + { + private const float corner_radius = 10; + + private const float left_edge_x_offset = 20f; + private const float keyboard_active_x_offset = 25f; + private const float active_x_offset = 50f; + + private const float duration = 500; + + protected float PanelXOffset { get; init; } + + private Box backgroundBorder = null!; + private Box backgroundGradient = null!; + private Box backgroundAccentGradient = null!; + private Container backgroundLayer = null!; + private Container backgroundLayerHorizontalPadding = null!; + private Container backgroundContainer = null!; + private Container iconContainer = null!; + private Box activationFlash = null!; + private Box hoverLayer = null!; + + public Container TopLevelContent { get; private set; } = null!; + + protected Container Content { get; private set; } = null!; + + public Drawable Background { set => backgroundContainer.Child = value; } + + public Drawable Icon { set => iconContainer.Child = value; } + + private Color4? accentColour; + + public Color4? AccentColour + { + get => accentColour; + set + { + accentColour = value; + updateDisplay(); + } + } + + [BackgroundDependencyLoader] + private void load(OverlayColourProvider colourProvider, OsuColour colours) + { + Anchor = Anchor.TopRight; + Origin = Anchor.TopRight; + + RelativeSizeAxes = Axes.X; + Height = CarouselItem.DEFAULT_HEIGHT; + + InternalChild = TopLevelContent = new Container + { + Masking = true, + CornerRadius = corner_radius, + RelativeSizeAxes = Axes.Both, + X = corner_radius, + EdgeEffect = new EdgeEffectParameters + { + Type = EdgeEffectType.Shadow, + Offset = new Vector2(1f), + Radius = 10, + }, + Children = new Drawable[] + { + new BufferedContainer + { + RelativeSizeAxes = Axes.Both, + Children = new Drawable[] + { + backgroundBorder = new Box + { + RelativeSizeAxes = Axes.Both, + Colour = Color4.White, + }, + backgroundLayerHorizontalPadding = new Container + { + RelativeSizeAxes = Axes.Both, + Child = backgroundLayer = new Container + { + RelativeSizeAxes = Axes.Both, + Child = new Container + { + Masking = true, + CornerRadius = corner_radius, + RelativeSizeAxes = Axes.Both, + Children = new Drawable[] + { + backgroundGradient = new Box + { + RelativeSizeAxes = Axes.Both, + }, + backgroundAccentGradient = new Box + { + RelativeSizeAxes = Axes.Both, + }, + backgroundContainer = new Container + { + RelativeSizeAxes = Axes.Both, + }, + } + }, + }, + } + }, + }, + iconContainer = new Container + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + AutoSizeAxes = Axes.Both, + }, + Content = new Container + { + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding { Right = PanelXOffset + corner_radius }, + }, + hoverLayer = new Box + { + Alpha = 0, + Blending = BlendingParameters.Additive, + RelativeSizeAxes = Axes.Both, + }, + activationFlash = new Box + { + Colour = Color4.White.Opacity(0.4f), + Blending = BlendingParameters.Additive, + Alpha = 0f, + RelativeSizeAxes = Axes.Both, + }, + new HoverSounds(), + } + }; + + hoverLayer.Colour = colours.Blue.Opacity(0.1f); + backgroundGradient.Colour = ColourInfo.GradientHorizontal(colourProvider.Background3, colourProvider.Background4); + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + Expanded.BindValueChanged(_ => updateDisplay()); + KeyboardSelected.BindValueChanged(_ => updateDisplay(), true); + } + + protected override void PrepareForUse() + { + base.PrepareForUse(); + this.FadeInFromZero(duration, Easing.OutQuint); + } + + [Resolved] + private BeatmapCarousel? carousel { get; set; } + + protected override bool OnClick(ClickEvent e) + { + activationFlash.FadeOutFromOne(500, Easing.OutQuint); + carousel?.Activate(Item!); + return true; + } + + private void updateDisplay() + { + backgroundLayer.TransformTo(nameof(Padding), backgroundLayer.Padding with { Vertical = Expanded.Value ? 2f : 0f }, duration, Easing.OutQuint); + + var backgroundColour = accentColour ?? Color4.White; + var edgeEffectColour = accentColour ?? Color4Extensions.FromHex(@"4EBFFF"); + + 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(); + } + + private void updateXOffset() + { + float x = PanelXOffset + active_x_offset + keyboard_active_x_offset + left_edge_x_offset; + + if (Expanded.Value) + x -= active_x_offset; + + if (KeyboardSelected.Value) + x -= keyboard_active_x_offset; + + this.TransformTo(nameof(Padding), new MarginPadding { Left = x }, duration, Easing.OutQuint); + } + + private void updateHover() + { + bool hovered = IsHovered || KeyboardSelected.Value; + + if (hovered) + hoverLayer.FadeIn(100, Easing.OutQuint); + else + hoverLayer.FadeOut(1000, Easing.OutQuint); + } + + protected override bool OnHover(HoverEvent e) + { + updateDisplay(); + return true; + } + + protected override void OnHoverLost(HoverLostEvent e) + { + updateDisplay(); + base.OnHoverLost(e); + } + + protected override void Update() + { + base.Update(); + Content.Padding = Content.Padding with { Left = iconContainer.DrawWidth }; + backgroundLayerHorizontalPadding.Padding = new MarginPadding { Left = iconContainer.DrawWidth }; + } + + #region ICarouselPanel + + public CarouselItem? Item { get; set; } + public BindableBool Selected { get; } = new BindableBool(); + public BindableBool Expanded { get; } = new BindableBool(); + public BindableBool KeyboardSelected { get; } = new BindableBool(); + + public double DrawYPosition { get; set; } + + public virtual void Activated() + { + activationFlash.FadeOutFromOne(500, Easing.OutQuint); + } + + #endregion + } +} diff --git a/osu.Game/Screens/SelectV2/PanelBeatmap.cs b/osu.Game/Screens/SelectV2/PanelBeatmap.cs new file mode 100644 index 000000000000..b27e5cae1492 --- /dev/null +++ b/osu.Game/Screens/SelectV2/PanelBeatmap.cs @@ -0,0 +1,246 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Collections.Generic; +using System.Diagnostics; +using System.Threading; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Game.Beatmaps; +using osu.Game.Beatmaps.Drawables; +using osu.Game.Graphics; +using osu.Game.Graphics.Containers; +using osu.Game.Graphics.Sprites; +using osu.Game.Graphics.UserInterface; +using osu.Game.Overlays; +using osu.Game.Resources.Localisation.Web; +using osu.Game.Rulesets; +using osu.Game.Rulesets.Mods; +using osuTK; + +namespace osu.Game.Screens.SelectV2 +{ + public partial class PanelBeatmap : PanelBase + { + public const float HEIGHT = CarouselItem.DEFAULT_HEIGHT; + + private StarCounter starCounter = null!; + private ConstrainedIconContainer difficultyIcon = null!; + private OsuSpriteText keyCountText = null!; + private StarRatingDisplay starRatingDisplay = null!; + private TopLocalRank difficultyRank = null!; + private OsuSpriteText difficultyText = null!; + private OsuSpriteText authorText = null!; + + private IBindable? starDifficultyBindable; + private CancellationTokenSource? starDifficultyCancellationSource; + + [Resolved] + private OverlayColourProvider colourProvider { get; set; } = null!; + + [Resolved] + private OsuColour colours { get; set; } = null!; + + [Resolved] + private BeatmapDifficultyCache difficultyCache { get; set; } = null!; + + [Resolved] + private IBindable ruleset { get; set; } = null!; + + [Resolved] + private IBindable> mods { get; set; } = null!; + + public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) + { + var inputRectangle = DrawRectangle; + + // 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 }); + + return inputRectangle.Contains(ToLocalSpace(screenSpacePos)); + } + + [BackgroundDependencyLoader] + private void load(OverlayColourProvider colourProvider) + { + Height = HEIGHT; + + Icon = difficultyIcon = new ConstrainedIconContainer + { + Size = new Vector2(20), + Margin = new MarginPadding { Horizontal = 5f }, + Colour = colourProvider.Background5, + }; + + Content.Children = new[] + { + new FillFlowContainer + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Padding = new MarginPadding { Left = 10f }, + Direction = FillDirection.Vertical, + AutoSizeAxes = Axes.Both, + Children = new Drawable[] + { + new FillFlowContainer + { + Direction = FillDirection.Horizontal, + Spacing = new Vector2(3, 0), + AutoSizeAxes = Axes.Both, + Children = new Drawable[] + { + starRatingDisplay = new StarRatingDisplay(default, StarRatingDisplaySize.Small) + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + }, + difficultyRank = new TopLocalRank + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Scale = new Vector2(0.75f) + }, + starCounter = new StarCounter + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Scale = new Vector2(0.4f) + } + } + }, + new FillFlowContainer + { + Direction = FillDirection.Horizontal, + AutoSizeAxes = Axes.Both, + Children = new[] + { + keyCountText = new OsuSpriteText + { + Font = OsuFont.GetFont(size: 18, weight: FontWeight.SemiBold), + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + Alpha = 0, + }, + difficultyText = new OsuSpriteText + { + Font = OsuFont.GetFont(size: 18, weight: FontWeight.SemiBold), + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + Margin = new MarginPadding { Right = 8f }, + }, + authorText = new OsuSpriteText + { + Colour = colourProvider.Content2, + Font = OsuFont.GetFont(weight: FontWeight.SemiBold), + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft + } + } + } + } + }, + }; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + ruleset.BindValueChanged(_ => + { + computeStarRating(); + updateKeyCount(); + }); + + mods.BindValueChanged(_ => + { + computeStarRating(); + updateKeyCount(); + }, true); + + Selected.BindValueChanged(s => Expanded.Value = s.NewValue, true); + } + + protected override void PrepareForUse() + { + base.PrepareForUse(); + + Debug.Assert(Item != null); + var beatmap = (BeatmapInfo)Item.Model; + + difficultyIcon.Icon = beatmap.Ruleset.CreateInstance().CreateIcon(); + + difficultyRank.Beatmap = beatmap; + difficultyText.Text = beatmap.DifficultyName; + authorText.Text = BeatmapsetsStrings.ShowDetailsMappedBy(beatmap.Metadata.Author.Username); + + computeStarRating(); + updateKeyCount(); + } + + protected override void FreeAfterUse() + { + base.FreeAfterUse(); + + difficultyRank.Beatmap = null; + starDifficultyBindable = null; + } + + private void computeStarRating() + { + starDifficultyCancellationSource?.Cancel(); + starDifficultyCancellationSource = new CancellationTokenSource(); + + if (Item == null) + return; + + var beatmap = (BeatmapInfo)Item.Model; + + starDifficultyBindable = difficultyCache.GetBindableDifficulty(beatmap, starDifficultyCancellationSource.Token); + starDifficultyBindable.BindValueChanged(_ => updateDisplay(), true); + } + + private void updateKeyCount() + { + if (Item == null) + return; + + var beatmap = (BeatmapInfo)Item.Model; + + if (ruleset.Value.OnlineID == 3) + { + // Account for mania differences locally for now. + // Eventually this should be handled in a more modular way, allowing rulesets to add more information to the panel. + ILegacyRuleset legacyRuleset = (ILegacyRuleset)ruleset.Value.CreateInstance(); + int keyCount = legacyRuleset.GetKeyCount(beatmap, mods.Value); + + keyCountText.Alpha = 1; + keyCountText.Text = $"[{keyCount}K] "; + } + else + 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 > 6.5f ? colours.Orange1 : colourProvider.Background5, duration, Easing.OutQuint); + + var starRatingColour = colours.ForStarDifficulty(starDifficulty.Stars); + starCounter.FadeColour(starRatingColour, duration, Easing.OutQuint); + AccentColour = starRatingColour; + } + } +} diff --git a/osu.Game/Screens/SelectV2/PanelBeatmapSet.cs b/osu.Game/Screens/SelectV2/PanelBeatmapSet.cs new file mode 100644 index 000000000000..5c38fe8e043e --- /dev/null +++ b/osu.Game/Screens/SelectV2/PanelBeatmapSet.cs @@ -0,0 +1,164 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Diagnostics; +using System.Linq; +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Localisation; +using osu.Game.Beatmaps; +using osu.Game.Beatmaps.Drawables; +using osu.Game.Graphics; +using osu.Game.Graphics.Sprites; +using osu.Game.Overlays; +using osuTK; + +namespace osu.Game.Screens.SelectV2 +{ + public partial class PanelBeatmapSet : PanelBase + { + public const float HEIGHT = CarouselItem.DEFAULT_HEIGHT * 1.6f; + + private BeatmapSetPanelBackground background = null!; + + private OsuSpriteText titleText = null!; + private OsuSpriteText artistText = null!; + private Drawable chevronIcon = null!; + private UpdateBeatmapSetButton updateButton = null!; + private BeatmapSetOnlineStatusPill statusPill = null!; + private DifficultySpectrumDisplay difficultiesDisplay = null!; + + [Resolved] + private OverlayColourProvider colourProvider { get; set; } = null!; + + [Resolved] + private BeatmapManager beatmaps { get; set; } = null!; + + public PanelBeatmapSet() + { + PanelXOffset = 20f; + } + + [BackgroundDependencyLoader] + private void load() + { + Height = HEIGHT; + + Icon = chevronIcon = new Container + { + Size = new Vector2(22), + Child = new SpriteIcon + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Icon = FontAwesome.Solid.ChevronRight, + Size = new Vector2(12), + X = 1f, + Colour = colourProvider.Background5, + }, + }; + + Background = background = new BeatmapSetPanelBackground + { + RelativeSizeAxes = Axes.Both, + }; + + Content.Children = new[] + { + new FillFlowContainer + { + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Vertical, + Padding = new MarginPadding { Top = 7.5f, Left = 15, Bottom = 5 }, + Children = new Drawable[] + { + titleText = new OsuSpriteText + { + Font = OsuFont.GetFont(weight: FontWeight.Bold, size: 22, italics: true), + }, + artistText = new OsuSpriteText + { + Font = OsuFont.GetFont(weight: FontWeight.SemiBold, size: 17, italics: true), + }, + new FillFlowContainer + { + Direction = FillDirection.Horizontal, + AutoSizeAxes = Axes.Both, + Margin = new MarginPadding { Top = 5f }, + Children = new Drawable[] + { + updateButton = new UpdateBeatmapSetButton + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Margin = new MarginPadding { Right = 5f, Top = -2f }, + }, + statusPill = new BeatmapSetOnlineStatusPill + { + AutoSizeAxes = Axes.Both, + Origin = Anchor.CentreLeft, + Anchor = Anchor.CentreLeft, + TextSize = 11, + TextPadding = new MarginPadding { Horizontal = 8, Vertical = 2 }, + Margin = new MarginPadding { Right = 5f }, + }, + difficultiesDisplay = new DifficultySpectrumDisplay + { + DotSize = new Vector2(5, 10), + DotSpacing = 2, + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + }, + }, + } + } + } + }; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + Expanded.BindValueChanged(_ => onExpanded(), true); + KeyboardSelected.BindValueChanged(k => KeyboardSelected.Value = k.NewValue, true); + } + + private void onExpanded() + { + const float duration = 500; + + chevronIcon.ResizeWidthTo(Expanded.Value ? 22 : 0f, duration, Easing.OutQuint); + chevronIcon.FadeTo(Expanded.Value ? 1f : 0f, duration, Easing.OutQuint); + } + + protected override void PrepareForUse() + { + base.PrepareForUse(); + + Debug.Assert(Item != null); + + var beatmapSet = (BeatmapSetInfo)Item.Model; + + // Choice of background image matches BSS implementation (always uses the lowest `beatmap_id` from the set). + background.Beatmap = beatmaps.GetWorkingBeatmap(beatmapSet.Beatmaps.MinBy(b => b.OnlineID)); + + titleText.Text = new RomanisableString(beatmapSet.Metadata.TitleUnicode, beatmapSet.Metadata.Title); + artistText.Text = new RomanisableString(beatmapSet.Metadata.ArtistUnicode, beatmapSet.Metadata.Artist); + updateButton.BeatmapSet = beatmapSet; + statusPill.Status = beatmapSet.Status; + difficultiesDisplay.BeatmapSet = beatmapSet; + } + + protected override void FreeAfterUse() + { + base.FreeAfterUse(); + + background.Beatmap = null; + updateButton.BeatmapSet = null; + difficultiesDisplay.BeatmapSet = null; + } + } +} diff --git a/osu.Game/Screens/SelectV2/PanelBeatmapStandalone.cs b/osu.Game/Screens/SelectV2/PanelBeatmapStandalone.cs new file mode 100644 index 000000000000..948311a86ecf --- /dev/null +++ b/osu.Game/Screens/SelectV2/PanelBeatmapStandalone.cs @@ -0,0 +1,281 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Collections.Generic; +using System.Diagnostics; +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.Localisation; +using osu.Game.Beatmaps; +using osu.Game.Beatmaps.Drawables; +using osu.Game.Graphics; +using osu.Game.Graphics.Containers; +using osu.Game.Graphics.Sprites; +using osu.Game.Overlays; +using osu.Game.Resources.Localisation.Web; +using osu.Game.Rulesets; +using osu.Game.Rulesets.Mods; +using osuTK; + +namespace osu.Game.Screens.SelectV2 +{ + public partial class PanelBeatmapStandalone : PanelBase + { + public const float HEIGHT = CarouselItem.DEFAULT_HEIGHT * 1.6f; + + [Resolved] + private IBindable ruleset { get; set; } = null!; + + [Resolved] + private IBindable> mods { get; set; } = null!; + + [Resolved] + private OverlayColourProvider colourProvider { get; set; } = null!; + + [Resolved] + private BeatmapManager beatmaps { get; set; } = null!; + + [Resolved] + private OsuColour colours { get; set; } = null!; + + [Resolved] + private BeatmapDifficultyCache difficultyCache { get; set; } = null!; + + private IBindable? starDifficultyBindable; + private CancellationTokenSource? starDifficultyCancellationSource; + + private BeatmapSetPanelBackground background = null!; + + private OsuSpriteText titleText = null!; + private OsuSpriteText artistText = null!; + private UpdateBeatmapSetButton updateButton = null!; + private BeatmapSetOnlineStatusPill statusPill = null!; + + private ConstrainedIconContainer difficultyIcon = null!; + private FillFlowContainer difficultyLine = null!; + private StarRatingDisplay difficultyStarRating = null!; + private TopLocalRank difficultyRank = null!; + private OsuSpriteText difficultyKeyCountText = null!; + private OsuSpriteText difficultyName = null!; + private OsuSpriteText difficultyAuthor = null!; + + public PanelBeatmapStandalone() + { + PanelXOffset = 20; + } + + [BackgroundDependencyLoader] + private void load() + { + Height = HEIGHT; + + Icon = difficultyIcon = new ConstrainedIconContainer + { + Size = new Vector2(20), + Margin = new MarginPadding { Horizontal = 5f }, + Colour = colourProvider.Background5, + }; + + Background = background = new BeatmapSetPanelBackground + { + RelativeSizeAxes = Axes.Both, + }; + + Content.Child = new FillFlowContainer + { + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Vertical, + Padding = new MarginPadding { Top = 7.5f, Left = 15, Bottom = 5 }, + Children = new Drawable[] + { + titleText = new OsuSpriteText + { + Font = OsuFont.GetFont(weight: FontWeight.Bold, size: 22, italics: true), + Shadow = true, + }, + artistText = new OsuSpriteText + { + Font = OsuFont.GetFont(weight: FontWeight.SemiBold, size: 17, italics: true), + Shadow = true, + }, + new FillFlowContainer + { + Direction = FillDirection.Horizontal, + AutoSizeAxes = Axes.Both, + Margin = new MarginPadding { Top = 5f }, + Children = new Drawable[] + { + updateButton = new UpdateBeatmapSetButton + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Margin = new MarginPadding { Right = 5f, Top = -2f }, + }, + statusPill = new BeatmapSetOnlineStatusPill + { + AutoSizeAxes = Axes.Both, + Origin = Anchor.CentreLeft, + Anchor = Anchor.CentreLeft, + TextSize = 11, + TextPadding = new MarginPadding { Horizontal = 8, Vertical = 2 }, + Margin = new MarginPadding { Right = 5f }, + }, + difficultyLine = new FillFlowContainer + { + Direction = FillDirection.Horizontal, + AutoSizeAxes = Axes.Both, + Children = new Drawable[] + { + difficultyStarRating = new StarRatingDisplay(default, StarRatingDisplaySize.Small) + { + Origin = Anchor.CentreLeft, + Anchor = Anchor.CentreLeft, + Scale = new Vector2(8f / 9f), + Margin = new MarginPadding { Right = 5f }, + }, + difficultyRank = new TopLocalRank + { + Scale = new Vector2(8f / 11), + Origin = Anchor.CentreLeft, + Anchor = Anchor.CentreLeft, + Margin = new MarginPadding { Right = 5f }, + }, + difficultyKeyCountText = new OsuSpriteText + { + Font = OsuFont.GetFont(size: 18, weight: FontWeight.SemiBold), + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + Alpha = 0, + Margin = new MarginPadding { Bottom = 2f }, + }, + difficultyName = new OsuSpriteText + { + Font = OsuFont.GetFont(size: 18, weight: FontWeight.SemiBold), + Origin = Anchor.BottomLeft, + Anchor = Anchor.BottomLeft, + Margin = new MarginPadding { Right = 5f, Bottom = 2f }, + }, + difficultyAuthor = new OsuSpriteText + { + Colour = colourProvider.Content2, + Font = OsuFont.GetFont(weight: FontWeight.SemiBold), + Origin = Anchor.BottomLeft, + Anchor = Anchor.BottomLeft, + Margin = new MarginPadding { Right = 5f, Bottom = 2f }, + } + } + }, + }, + } + } + }; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + ruleset.BindValueChanged(_ => + { + computeStarRating(); + updateKeyCount(); + }); + + mods.BindValueChanged(_ => + { + computeStarRating(); + updateKeyCount(); + }, true); + + Selected.BindValueChanged(s => Expanded.Value = s.NewValue, true); + } + + protected override void PrepareForUse() + { + base.PrepareForUse(); + + Debug.Assert(Item != null); + + var beatmap = (BeatmapInfo)Item.Model; + var beatmapSet = beatmap.BeatmapSet!; + + // Choice of background image matches BSS implementation (always uses the lowest `beatmap_id` from the set). + background.Beatmap = beatmaps.GetWorkingBeatmap(beatmapSet.Beatmaps.MinBy(b => b.OnlineID)); + + titleText.Text = new RomanisableString(beatmapSet.Metadata.TitleUnicode, beatmapSet.Metadata.Title); + artistText.Text = new RomanisableString(beatmapSet.Metadata.ArtistUnicode, beatmapSet.Metadata.Artist); + updateButton.BeatmapSet = beatmapSet; + statusPill.Status = beatmapSet.Status; + + difficultyIcon.Icon = beatmap.Ruleset.CreateInstance().CreateIcon(); + difficultyIcon.Show(); + + difficultyRank.Beatmap = beatmap; + difficultyName.Text = beatmap.DifficultyName; + difficultyAuthor.Text = BeatmapsetsStrings.ShowDetailsMappedBy(beatmap.Metadata.Author.Username); + difficultyLine.Show(); + + computeStarRating(); + } + + protected override void FreeAfterUse() + { + base.FreeAfterUse(); + + background.Beatmap = null; + updateButton.BeatmapSet = null; + difficultyRank.Beatmap = null; + starDifficultyBindable = null; + } + + private void computeStarRating() + { + starDifficultyCancellationSource?.Cancel(); + starDifficultyCancellationSource = new CancellationTokenSource(); + + if (Item == null) + return; + + var beatmap = (BeatmapInfo)Item.Model; + + starDifficultyBindable = difficultyCache.GetBindableDifficulty(beatmap, starDifficultyCancellationSource.Token); + starDifficultyBindable.BindValueChanged(_ => updateDisplay(), true); + } + + private void updateKeyCount() + { + if (Item == null) + return; + + var beatmap = (BeatmapInfo)Item.Model; + + if (ruleset.Value.OnlineID == 3) + { + // Account for mania differences locally for now. + // Eventually this should be handled in a more modular way, allowing rulesets to add more information to the panel. + ILegacyRuleset legacyRuleset = (ILegacyRuleset)ruleset.Value.CreateInstance(); + int keyCount = legacyRuleset.GetKeyCount(beatmap, mods.Value); + + difficultyKeyCountText.Alpha = 1; + difficultyKeyCountText.Text = $"[{keyCount}K] "; + } + else + difficultyKeyCountText.Alpha = 0; + } + + private void updateDisplay() + { + const float duration = 500; + + var starDifficulty = starDifficultyBindable?.Value ?? default; + + AccentColour = colours.ForStarDifficulty(starDifficulty.Stars); + difficultyIcon.FadeColour(starDifficulty.Stars > 6.5f ? colours.Orange1 : colourProvider.Background5, duration, Easing.OutQuint); + difficultyStarRating.Current.Value = starDifficulty; + } + } +} diff --git a/osu.Game/Screens/SelectV2/PanelGroup.cs b/osu.Game/Screens/SelectV2/PanelGroup.cs new file mode 100644 index 000000000000..ecb64f479795 --- /dev/null +++ b/osu.Game/Screens/SelectV2/PanelGroup.cs @@ -0,0 +1,108 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Diagnostics; +using osu.Framework.Allocation; +using osu.Framework.Extensions.Color4Extensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.Sprites; +using osu.Game.Graphics; +using osu.Game.Graphics.Sprites; +using osu.Game.Overlays; +using osuTK; +using osuTK.Graphics; + +namespace osu.Game.Screens.SelectV2 +{ + public partial class PanelGroup : PanelBase + { + public const float HEIGHT = CarouselItem.DEFAULT_HEIGHT; + + private Drawable chevronIcon = null!; + private OsuSpriteText titleText = null!; + + [BackgroundDependencyLoader] + private void load(OverlayColourProvider colourProvider) + { + Height = HEIGHT; + + Icon = chevronIcon = new SpriteIcon + { + AlwaysPresent = true, + Icon = FontAwesome.Solid.ChevronDown, + Size = new Vector2(12), + Margin = new MarginPadding { Horizontal = 5f }, + X = 2f, + Colour = colourProvider.Background3, + }; + Background = new Box + { + RelativeSizeAxes = Axes.Both, + Colour = colourProvider.Dark1, + }; + AccentColour = colourProvider.Highlight1; + Content.Children = new Drawable[] + { + titleText = new OsuSpriteText + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + X = 10f, + }, + new CircularContainer + { + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, + Size = new Vector2(50f, 14f), + Margin = new MarginPadding { Right = 20f }, + Masking = true, + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = Color4.Black.Opacity(0.7f), + }, + new OsuSpriteText + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Font = OsuFont.Torus.With(size: 14.4f, weight: FontWeight.Bold), + // TODO: requires Carousel/CarouselItem-side implementation + Text = "43", + UseFullGlyphHeight = false, + } + }, + } + }; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + Expanded.BindValueChanged(_ => onExpanded(), true); + } + + private void onExpanded() + { + const float duration = 500; + + chevronIcon.ResizeWidthTo(Expanded.Value ? 12f : 0f, duration, Easing.OutQuint); + chevronIcon.FadeTo(Expanded.Value ? 1f : 0f, duration, Easing.OutQuint); + } + + protected override void PrepareForUse() + { + base.PrepareForUse(); + + Debug.Assert(Item != null); + + GroupDefinition group = (GroupDefinition)Item.Model; + + titleText.Text = group.Title; + } + } +} diff --git a/osu.Game/Screens/SelectV2/PanelGroupStarDifficulty.cs b/osu.Game/Screens/SelectV2/PanelGroupStarDifficulty.cs new file mode 100644 index 000000000000..0dc5a2f36588 --- /dev/null +++ b/osu.Game/Screens/SelectV2/PanelGroupStarDifficulty.cs @@ -0,0 +1,142 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Diagnostics; +using osu.Framework.Allocation; +using osu.Framework.Extensions.Color4Extensions; +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.Beatmaps.Drawables; +using osu.Game.Graphics; +using osu.Game.Graphics.Sprites; +using osu.Game.Graphics.UserInterface; +using osu.Game.Overlays; +using osuTK; +using osuTK.Graphics; + +namespace osu.Game.Screens.SelectV2 +{ + public partial class PanelGroupStarDifficulty : PanelBase + { + [Resolved] + private OsuColour colours { get; set; } = null!; + + [Resolved] + private OverlayColourProvider colourProvider { get; set; } = null!; + + private Drawable chevronIcon = null!; + private Box contentBackground = null!; + private StarRatingDisplay starRatingDisplay = null!; + private StarCounter starCounter = null!; + + [BackgroundDependencyLoader] + private void load() + { + Height = PanelGroup.HEIGHT; + + Icon = chevronIcon = new SpriteIcon + { + AlwaysPresent = true, + Icon = FontAwesome.Solid.ChevronDown, + Size = new Vector2(12), + Margin = new MarginPadding { Horizontal = 5f }, + X = 2f, + }; + Background = contentBackground = new Box + { + RelativeSizeAxes = Axes.Both, + Colour = colourProvider.Dark1, + }; + AccentColour = colourProvider.Highlight1; + Content.Children = new Drawable[] + { + new FillFlowContainer + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + AutoSizeAxes = Axes.Both, + Spacing = new Vector2(10f, 0f), + Margin = new MarginPadding { Left = 10f }, + Children = new Drawable[] + { + starRatingDisplay = new StarRatingDisplay(default, StarRatingDisplaySize.Small) + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + }, + starCounter = new StarCounter + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Scale = new Vector2(8f / 20f), + }, + } + }, + new CircularContainer + { + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, + Size = new Vector2(50f, 14f), + Margin = new MarginPadding { Right = 20f }, + Masking = true, + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = Color4.Black.Opacity(0.7f), + }, + new OsuSpriteText + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Font = OsuFont.Torus.With(size: 14.4f, weight: FontWeight.Bold), + // TODO: requires Carousel/CarouselItem-side implementation + Text = "43", + UseFullGlyphHeight = false, + } + }, + } + }; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + Expanded.BindValueChanged(_ => onExpanded(), true); + } + + protected override void PrepareForUse() + { + base.PrepareForUse(); + + Debug.Assert(Item != null); + + int starNumber = (int)((GroupDefinition)Item.Model).Data; + + Color4 colour = starNumber >= 9 ? OsuColour.Gray(0.2f) : colours.ForStarDifficulty(starNumber); + Color4 contentColour = starNumber >= 7 ? colours.Orange1 : colourProvider.Background5; + + AccentColour = colour; + contentBackground.Colour = colour.Darken(0.3f); + + starRatingDisplay.Current.Value = new StarDifficulty(starNumber, 0); + starCounter.Current = starNumber; + + chevronIcon.Colour = contentColour; + starCounter.Colour = contentColour; + } + + private void onExpanded() + { + const float duration = 500; + + chevronIcon.ResizeWidthTo(Expanded.Value ? 12f : 0f, duration, Easing.OutQuint); + chevronIcon.FadeTo(Expanded.Value ? 1f : 0f, duration, Easing.OutQuint); + } + } +} diff --git a/osu.Game/Screens/SelectV2/SongSelectV2.cs b/osu.Game/Screens/SelectV2/SongSelectV2.cs index 2f9667793fe5..3943d059f97b 100644 --- a/osu.Game/Screens/SelectV2/SongSelectV2.cs +++ b/osu.Game/Screens/SelectV2/SongSelectV2.cs @@ -39,6 +39,18 @@ private void load() { AddRangeInternal(new Drawable[] { + new Container + { + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding { Bottom = ScreenFooter.HEIGHT }, + Child = new BeatmapCarousel + { + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight, + RelativeSizeAxes = Axes.Both, + Width = 0.6f, + }, + }, modSelectOverlay, }); } diff --git a/osu.Game/Screens/SelectV2/TopLocalRank.cs b/osu.Game/Screens/SelectV2/TopLocalRank.cs new file mode 100644 index 000000000000..2a72a05db75e --- /dev/null +++ b/osu.Game/Screens/SelectV2/TopLocalRank.cs @@ -0,0 +1,106 @@ +// 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.Linq; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Game.Beatmaps; +using osu.Game.Database; +using osu.Game.Models; +using osu.Game.Online.API; +using osu.Game.Online.Leaderboards; +using osu.Game.Rulesets; +using osu.Game.Scoring; +using osuTK; +using Realms; + +namespace osu.Game.Screens.SelectV2 +{ + public partial class TopLocalRank : CompositeDrawable + { + private BeatmapInfo? beatmap; + + public BeatmapInfo? Beatmap + { + get => beatmap; + set + { + beatmap = value; + + if (IsLoaded) + updateSubscription(); + } + } + + [Resolved] + private IBindable ruleset { get; set; } = null!; + + [Resolved] + private RealmAccess realm { get; set; } = null!; + + [Resolved] + private IAPIProvider api { get; set; } = null!; + + private IDisposable? scoreSubscription; + + private readonly UpdateableRank updateable; + + public TopLocalRank(BeatmapInfo? beatmap = null) + { + AutoSizeAxes = Axes.Both; + + InternalChild = updateable = new UpdateableRank + { + Size = new Vector2(40, 20), + Alpha = 0, + }; + + Beatmap = beatmap; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + ruleset.BindValueChanged(_ => updateSubscription(), true); + } + + private void updateSubscription() + { + scoreSubscription?.Dispose(); + + if (beatmap == null) + return; + + scoreSubscription = realm.RegisterForNotifications(r => + r.All() + .Filter($"{nameof(ScoreInfo.User)}.{nameof(RealmUser.OnlineID)} == $0" + + $" && {nameof(ScoreInfo.BeatmapInfo)}.{nameof(BeatmapInfo.ID)} == $1" + + $" && {nameof(ScoreInfo.BeatmapInfo)}.{nameof(BeatmapInfo.Hash)} == {nameof(ScoreInfo.BeatmapHash)}" + + $" && {nameof(ScoreInfo.Ruleset)}.{nameof(RulesetInfo.ShortName)} == $2" + + $" && {nameof(ScoreInfo.DeletePending)} == false", api.LocalUser.Value.Id, beatmap.ID, ruleset.Value.ShortName), + localScoresChanged); + } + + private void localScoresChanged(IRealmCollection sender, ChangeSet? changes) + { + // This subscription may fire from changes to linked beatmaps, which we don't care about. + // It's currently not possible for a score to be modified after insertion, so we can safely ignore callbacks with only modifications. + if (changes?.HasCollectionChanges() == false) + return; + + ScoreInfo? topScore = sender.MaxBy(info => (info.TotalScore, -info.Date.UtcDateTime.Ticks)); + updateable.Rank = topScore?.Rank; + updateable.Alpha = topScore != null ? 1 : 0; + } + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + scoreSubscription?.Dispose(); + } + } +} diff --git a/osu.Game/Screens/SelectV2/UpdateBeatmapSetButton.cs b/osu.Game/Screens/SelectV2/UpdateBeatmapSetButton.cs new file mode 100644 index 000000000000..e2c841f88afd --- /dev/null +++ b/osu.Game/Screens/SelectV2/UpdateBeatmapSetButton.cs @@ -0,0 +1,198 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Diagnostics; +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.Framework.Input.Events; +using osu.Game.Beatmaps; +using osu.Game.Configuration; +using osu.Game.Graphics; +using osu.Game.Graphics.Sprites; +using osu.Game.Graphics.UserInterface; +using osu.Game.Online.API; +using osu.Game.Overlays; +using osu.Game.Screens.Select.Carousel; +using osuTK; +using osuTK.Graphics; + +namespace osu.Game.Screens.SelectV2 +{ + public partial class UpdateBeatmapSetButton : OsuAnimatedButton + { + private BeatmapSetInfo? beatmapSet; + + public BeatmapSetInfo? BeatmapSet + { + get => beatmapSet; + set + { + beatmapSet = value; + + if (IsLoaded) + beatmapChanged(); + } + } + + private SpriteIcon icon = null!; + private Box progressFill = null!; + + [Resolved] + private BeatmapModelDownloader beatmapDownloader { get; set; } = null!; + + [Resolved] + private IAPIProvider api { get; set; } = null!; + + [Resolved] + private LoginOverlay? loginOverlay { get; set; } + + [Resolved] + private IDialogOverlay? dialogOverlay { get; set; } + + public UpdateBeatmapSetButton() + { + Size = new Vector2(75f, 22f); + } + + private Bindable preferNoVideo = null!; + + [BackgroundDependencyLoader] + private void load(OsuConfigManager config) + { + const float icon_size = 14; + + preferNoVideo = config.GetBindable(OsuSetting.PreferNoVideo); + + Content.Anchor = Anchor.Centre; + Content.Origin = Anchor.Centre; + Content.Shear = new Vector2(OsuGame.SHEAR, 0); + + Content.AddRange(new Drawable[] + { + progressFill = new Box + { + Colour = Color4.White, + Alpha = 0.2f, + Blending = BlendingParameters.Additive, + RelativeSizeAxes = Axes.Both, + Width = 0, + }, + new FillFlowContainer + { + Padding = new MarginPadding { Horizontal = 5, Vertical = 3 }, + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Horizontal, + Spacing = new Vector2(4), + Shear = new Vector2(-OsuGame.SHEAR, 0), + Children = new Drawable[] + { + new Container + { + Size = new Vector2(icon_size), + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Children = new Drawable[] + { + icon = new SpriteIcon + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Icon = FontAwesome.Solid.SyncAlt, + Size = new Vector2(icon_size), + }, + } + }, + new OsuSpriteText + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Font = OsuFont.Default.With(weight: FontWeight.Bold), + Text = "Update", + } + } + }, + }); + + Action = performUpdate; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + beatmapChanged(); + } + + private void beatmapChanged() + { + Alpha = beatmapSet?.AllBeatmapsUpToDate == false ? 1 : 0; + icon.Spin(4000, RotationDirection.Clockwise); + } + + protected override bool OnHover(HoverEvent e) + { + icon.Spin(400, RotationDirection.Clockwise); + return base.OnHover(e); + } + + protected override void OnHoverLost(HoverLostEvent e) + { + icon.Spin(4000, RotationDirection.Clockwise); + base.OnHoverLost(e); + } + + private bool updateConfirmed; + + private void performUpdate() + { + Debug.Assert(beatmapSet != null); + + if (!api.IsLoggedIn) + { + loginOverlay?.Show(); + return; + } + + if (dialogOverlay != null && beatmapSet.Status == BeatmapOnlineStatus.LocallyModified && !updateConfirmed) + { + dialogOverlay.Push(new UpdateLocalConfirmationDialog(() => + { + updateConfirmed = true; + performUpdate(); + })); + + return; + } + + updateConfirmed = false; + + beatmapDownloader.DownloadAsUpdate(beatmapSet, preferNoVideo.Value); + attachExistingDownload(); + } + + private void attachExistingDownload() + { + Debug.Assert(beatmapSet != null); + var download = beatmapDownloader.GetExistingDownload(beatmapSet); + + if (download != null) + { + Enabled.Value = false; + TooltipText = string.Empty; + + download.DownloadProgressed += progress => progressFill.ResizeWidthTo(progress, 100, Easing.OutQuint); + download.Failure += _ => attachExistingDownload(); + } + else + { + Enabled.Value = true; + TooltipText = "Update beatmap with online changes"; + + progressFill.ResizeWidthTo(0, 100, Easing.OutQuint); + } + } + } +}