diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs b/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs index 898417811c..046840a691 100644 --- a/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs +++ b/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs @@ -22,6 +22,7 @@ using osu.Game.Beatmaps; using osu.Game.Collections; using osu.Game.Configuration; using osu.Game.Extensions; +using osu.Game.Graphics.Carousel; using osu.Game.Graphics.Containers; using osu.Game.Graphics.UserInterface; using osu.Game.Online.API; @@ -52,11 +53,16 @@ using osu.Game.Screens.Select; using osu.Game.Screens.Select.Carousel; using osu.Game.Screens.Select.Leaderboards; using osu.Game.Screens.Select.Options; +using osu.Game.Screens.SelectV2; using osu.Game.Tests.Beatmaps.IO; using osu.Game.Tests.Resources; using osu.Game.Utils; using osuTK; using osuTK.Input; +using BeatmapCarousel = osu.Game.Screens.Select.BeatmapCarousel; +using CollectionDropdown = osu.Game.Collections.CollectionDropdown; +using FilterControl = osu.Game.Screens.Select.FilterControl; +using FooterButtonRandom = osu.Game.Screens.Select.FooterButtonRandom; namespace osu.Game.Tests.Visual.Navigation { @@ -274,6 +280,58 @@ namespace osu.Game.Tests.Visual.Navigation double getCarouselScrollPosition() => Game.ChildrenOfType>().Single().Current; } + [Test] + public void TestNewSongSelectScrollHandling() + { + SoloSongSelect songSelect = null; + double scrollPosition = 0; + + AddStep("set game volume to max", () => Game.Dependencies.Get().SetValue(FrameworkSetting.VolumeUniversal, 1d)); + AddUntilStep("wait for volume overlay to hide", () => Game.ChildrenOfType().SingleOrDefault()?.State.Value, () => Is.EqualTo(Visibility.Hidden)); + PushAndConfirm(() => songSelect = new SoloSongSelect()); + AddUntilStep("wait for song select", () => songSelect.IsLoaded); + AddStep("import beatmap", () => BeatmapImportHelper.LoadQuickOszIntoOsu(Game).WaitSafely()); + AddUntilStep("wait for beatmap", () => Game.ChildrenOfType().Any()); + + AddWaitStep("wait for scroll", 10); + + AddStep("store scroll position", () => scrollPosition = getCarouselScrollPosition()); + + AddStep("move to title wedge", () => InputManager.MoveMouseTo( + songSelect.ChildrenOfType().Single())); + AddStep("scroll down", () => InputManager.ScrollVerticalBy(-1)); + AddAssert("carousel didn't move", getCarouselScrollPosition, () => Is.EqualTo(scrollPosition)); + + AddRepeatStep("alt-scroll down", () => + { + InputManager.PressKey(Key.AltLeft); + InputManager.ScrollVerticalBy(-1); + InputManager.ReleaseKey(Key.AltLeft); + }, 5); + AddAssert("game volume decreased", () => Game.Dependencies.Get().Get(FrameworkSetting.VolumeUniversal), () => Is.LessThan(1)); + + AddStep("set game volume to max", () => Game.Dependencies.Get().SetValue(FrameworkSetting.VolumeUniversal, 1d)); + + AddStep("move to metadata wedge", () => InputManager.MoveMouseTo( + songSelect.ChildrenOfType().Single())); + AddStep("scroll down", () => InputManager.ScrollVerticalBy(-1)); + AddAssert("carousel didn't move", getCarouselScrollPosition, () => Is.EqualTo(scrollPosition)); + + AddRepeatStep("alt-scroll down", () => + { + InputManager.PressKey(Key.AltLeft); + InputManager.ScrollVerticalBy(-1); + InputManager.ReleaseKey(Key.AltLeft); + }, 5); + AddAssert("game volume decreased", () => Game.Dependencies.Get().Get(FrameworkSetting.VolumeUniversal), () => Is.LessThan(1)); + + AddStep("move to carousel", () => InputManager.MoveMouseTo(songSelect.ChildrenOfType().Single())); + AddStep("scroll down", () => InputManager.ScrollVerticalBy(-1)); + AddAssert("carousel moved", getCarouselScrollPosition, () => Is.Not.EqualTo(scrollPosition)); + + double getCarouselScrollPosition() => Game.ChildrenOfType>().Single().ChildrenOfType().Single().Current; + } + /// /// This tests that the F1 key will open the mod select overlay, and not be handled / blocked by the music controller (which has the same default binding /// but should be handled *after* song select). diff --git a/osu.Game/Graphics/Carousel/Carousel.cs b/osu.Game/Graphics/Carousel/Carousel.cs index 31c95f6930..9e0c0ed3c8 100644 --- a/osu.Game/Graphics/Carousel/Carousel.cs +++ b/osu.Game/Graphics/Carousel/Carousel.cs @@ -136,6 +136,17 @@ namespace osu.Game.Graphics.Carousel selectionValid.Invalidate(); } + /// + /// Scroll carousel to the selected item if available. + /// + public void ScrollToSelection() + { + // TODO: this likely needs to be delayed until currentKeyboardSelection has a valid value. + // Early calls to `ScrollToSelection` will currently silently fail. + if (currentKeyboardSelection.CarouselItem != null) + Scroll.ScrollTo(currentKeyboardSelection.CarouselItem.CarouselYPosition - visibleHalfHeight + BleedTop); + } + /// /// Returns the vertical spacing between two given carousel items. Negative value can be used to create an overlapping effect. /// @@ -316,7 +327,7 @@ namespace osu.Game.Graphics.Carousel refreshAfterSelection(); if (!Scroll.UserScrolling) - scrollToSelection(); + ScrollToSelection(); NewItemsPresented?.Invoke(carouselItems); }); @@ -553,12 +564,6 @@ namespace osu.Game.Graphics.Carousel Scroll.OffsetScrollPosition((float)(currentKeyboardSelection.YPosition!.Value - prevKeyboard.YPosition.Value)); } - private void scrollToSelection() - { - if (currentKeyboardSelection.CarouselItem != null) - Scroll.ScrollTo(currentKeyboardSelection.CarouselItem.CarouselYPosition - visibleHalfHeight + BleedTop); - } - #endregion #region Display handling @@ -598,7 +603,7 @@ namespace osu.Game.Graphics.Carousel refreshAfterSelection(); // Always scroll to selection in this case (regardless of `UserScrolling` state), centering the selection. - scrollToSelection(); + ScrollToSelection(); selectionValid.Validate(); } @@ -820,6 +825,11 @@ namespace osu.Game.Graphics.Carousel public void SetLayoutHeight(float height) => Panels.Height = height; + /// + /// Allow handling right click scroll outside of the carousel's display area. + /// + public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => true; + public CarouselScrollContainer() { // Managing our own custom layout within ScrollContent causes feedback with public ScrollContainer calculations, diff --git a/osu.Game/Screens/SelectV2/SongSelect.cs b/osu.Game/Screens/SelectV2/SongSelect.cs index d7dad86600..48cee76581 100644 --- a/osu.Game/Screens/SelectV2/SongSelect.cs +++ b/osu.Game/Screens/SelectV2/SongSelect.cs @@ -154,22 +154,43 @@ namespace osu.Game.Screens.SelectV2 { new[] { - wedgesContainer = new FillFlowContainer + new Container { RelativeSizeAxes = Axes.Both, - Margin = new MarginPadding - { - Top = -CORNER_RADIUS_HIDE_OFFSET, - Left = -CORNER_RADIUS_HIDE_OFFSET - }, - Spacing = new Vector2(0f, 4f), - Direction = FillDirection.Vertical, + // Ensure the left components are on top of the carousel both visually (although they should never overlay) + // but more importantly, for input purposes to allow the scroll-to-selection logic to override carousel's + // screen-wide scroll handling. + Depth = float.MinValue, Shear = OsuGame.SHEAR, Children = new Drawable[] { - new ShearAligningWrapper(titleWedge = new BeatmapTitleWedge()), - new ShearAligningWrapper(detailsArea = new BeatmapDetailsArea()), - }, + new Container + { + // Pad enough to only reset scroll when well into the left wedge areas. + Padding = new MarginPadding { Right = 40 }, + RelativeSizeAxes = Axes.Both, + Child = new Select.SongSelect.LeftSideInteractionContainer(() => carousel.ScrollToSelection()) + { + RelativeSizeAxes = Axes.Both, + }, + }, + wedgesContainer = new FillFlowContainer + { + RelativeSizeAxes = Axes.Both, + Margin = new MarginPadding + { + Top = -CORNER_RADIUS_HIDE_OFFSET, + Left = -CORNER_RADIUS_HIDE_OFFSET + }, + Spacing = new Vector2(0f, 4f), + Direction = FillDirection.Vertical, + Children = new Drawable[] + { + new ShearAligningWrapper(titleWedge = new BeatmapTitleWedge()), + new ShearAligningWrapper(detailsArea = new BeatmapDetailsArea()), + }, + }, + } }, Empty(), new Container