diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneSongSelect.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneSongSelect.cs index 3161e62683..5d96ebaa85 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestSceneSongSelect.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneSongSelect.cs @@ -108,6 +108,21 @@ namespace osu.Game.Tests.Visual.SongSelectV2 AddAssert("beatmap set deleted", () => Beatmaps.GetAllUsableBeatmapSets().Any(), () => Is.False); } + [Test] + public void TestClearModsViaModButtonRightClick() + { + LoadSongSelect(); + + AddStep("select NC", () => SelectedMods.Value = new[] { new OsuModNightcore() }); + AddAssert("mods selected", () => SelectedMods.Value, () => Has.Count.EqualTo(1)); + AddStep("right click mod button", () => + { + InputManager.MoveMouseTo(Footer.ChildrenOfType().Single()); + InputManager.Click(MouseButton.Right); + }); + AddAssert("not mods selected", () => SelectedMods.Value, () => Has.Count.EqualTo(0)); + } + [Test] public void TestSpeedChange() { diff --git a/osu.Game/Graphics/Carousel/Carousel.cs b/osu.Game/Graphics/Carousel/Carousel.cs index a9655aab56..37d69dab89 100644 --- a/osu.Game/Graphics/Carousel/Carousel.cs +++ b/osu.Game/Graphics/Carousel/Carousel.cs @@ -623,7 +623,7 @@ namespace osu.Game.Graphics.Carousel if (c.Item == null) continue; - float normalisedDepth = (float)(Math.Abs(selectedYPos - c.DrawYPosition) / DrawHeight); + float normalisedDepth = (float)(Math.Abs(selectedYPos - c.Item.CarouselYPosition) / DrawHeight); Scroll.Panels.ChangeChildDepth(panel, c.Item.DepthLayer + normalisedDepth); if (c.DrawYPosition != c.Item.CarouselYPosition) @@ -695,6 +695,12 @@ namespace osu.Game.Graphics.Carousel { var carouselPanel = (ICarouselPanel)panel; + if (carouselPanel.Item == null) + { + // Item is null when a panel is already fading away from existence; should be ignored for tracking purposes. + continue; + } + // The case where we're intending to display this panel, but it's already displayed. // Note that we **must compare the model here** as the CarouselItems may be fresh instances due to a filter operation. // @@ -710,7 +716,7 @@ namespace osu.Game.Graphics.Carousel } // If the new display range doesn't contain the panel, it's no longer required for display. - expirePanelImmediately(panel); + expirePanel(panel); } // Add any new items which need to be displayed and haven't yet. @@ -721,12 +727,36 @@ namespace osu.Game.Graphics.Carousel if (drawable is not ICarouselPanel carouselPanel) throw new InvalidOperationException($"Carousel panel drawables must implement {typeof(ICarouselPanel)}"); - carouselPanel.DrawYPosition = item.CarouselYPosition; carouselPanel.Item = item; - Scroll.Add(drawable); } + if (toDisplay.Any()) + { + // To make transitions of items appearing in the flow look good, do a pass and make sure newly added items spawn from + // just beneath the *current interpolated position* of the previous panel. + var orderedPanels = Scroll.Panels + .OfType() + .Where(p => p.Item != null) + .OrderBy(p => p.Item!.CarouselYPosition) + .ToList(); + + for (int i = 0; i < orderedPanels.Count; i++) + { + var panel = orderedPanels[i]; + + if (toDisplay.Contains(panel.Item!)) + { + // Don't apply to the last because animating the tail of the list looks bad. + // It's usually off-screen anyway. + if (i > 0 && i < orderedPanels.Count - 1) + panel.DrawYPosition = orderedPanels[i - 1].DrawYPosition; + else + panel.DrawYPosition = panel.Item!.CarouselYPosition; + } + } + } + // Update the total height of all items (to make the scroll container scrollable through the full height even though // most items are not displayed / loaded). if (carouselItems.Count > 0) @@ -738,13 +768,18 @@ namespace osu.Game.Graphics.Carousel Scroll.SetLayoutHeight(0); } - private static void expirePanelImmediately(Drawable panel) + private void expirePanel(Drawable panel) { - panel.FinishTransforms(); - panel.Expire(); - var carouselPanel = (ICarouselPanel)panel; + // expired panels should have a depth behind all other panels to make the transition not look weird. + Scroll.Panels.ChangeChildDepth(panel, panel.Depth + 1024); + + panel.FadeOut(150, Easing.OutQuint); + panel.MoveToX(panel.X + 100, 200, Easing.Out); + + panel.Expire(); + carouselPanel.Item = null; carouselPanel.Selected.Value = false; carouselPanel.KeyboardSelected.Value = false; @@ -801,12 +836,7 @@ namespace osu.Game.Graphics.Carousel base.OffsetScrollPosition(offset); foreach (var panel in Panels) - { - var c = (ICarouselPanel)panel; - Debug.Assert(c.Item != null); - - c.DrawYPosition += offset; - } + ((ICarouselPanel)panel).DrawYPosition += offset; } public override void Clear(bool disposeChildren) diff --git a/osu.Game/Screens/SelectV2/FooterButtonMods.cs b/osu.Game/Screens/SelectV2/FooterButtonMods.cs index 41ac1a2ff6..9e2b53012a 100644 --- a/osu.Game/Screens/SelectV2/FooterButtonMods.cs +++ b/osu.Game/Screens/SelectV2/FooterButtonMods.cs @@ -14,6 +14,7 @@ using osu.Framework.Graphics.Effects; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.UserInterface; +using osu.Framework.Input.Events; using osu.Framework.Localisation; using osu.Game.Configuration; using osu.Game.Graphics; @@ -27,11 +28,14 @@ using osu.Game.Screens.Play.HUD; using osu.Game.Utils; using osuTK; using osuTK.Graphics; +using osuTK.Input; namespace osu.Game.Screens.SelectV2 { public partial class FooterButtonMods : ScreenFooterButton, IHasCurrentValue> { + public Action? RequestDeselectAllMods { get; init; } + private const float bar_height = 30f; private const float mod_display_portion = 0.65f; @@ -172,6 +176,18 @@ namespace osu.Game.Screens.SelectV2 FinishTransforms(true); } + protected override bool OnMouseDown(MouseDownEvent e) + { + // should probably be OnClick but right mouse button clicks isn't setup well. + if (e.Button == MouseButton.Right) + { + RequestDeselectAllMods?.Invoke(); + return true; + } + + return base.OnMouseDown(e); + } + private const double duration = 240; private const Easing easing = Easing.OutQuint; diff --git a/osu.Game/Screens/SelectV2/SongSelect.cs b/osu.Game/Screens/SelectV2/SongSelect.cs index 1ab429b16e..ba93403036 100644 --- a/osu.Game/Screens/SelectV2/SongSelect.cs +++ b/osu.Game/Screens/SelectV2/SongSelect.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; @@ -22,6 +23,7 @@ using osu.Game.Input.Bindings; using osu.Game.Overlays; using osu.Game.Overlays.Mods; using osu.Game.Overlays.Volume; +using osu.Game.Rulesets.Mods; using osu.Game.Scoring; using osu.Game.Screens.Edit; using osu.Game.Screens.Footer; @@ -197,7 +199,11 @@ namespace osu.Game.Screens.SelectV2 public override IReadOnlyList CreateFooterButtons() => new ScreenFooterButton[] { - new FooterButtonMods(modSelectOverlay) { Current = Mods }, + new FooterButtonMods(modSelectOverlay) + { + Current = Mods, + RequestDeselectAllMods = () => Mods.Value = Array.Empty() + }, new FooterButtonRandom(), new FooterButtonOptions(), };