1
0
mirror of https://github.com/ppy/osu.git synced 2026-05-31 19:10:43 +08:00

Merge pull request #32220 from peppy/song-select-flows

Add ability to start gameplay from carousel v2
This commit is contained in:
Bartłomiej Dach
2025-03-06 10:47:55 +01:00
committed by GitHub
Unverified
12 changed files with 137 additions and 63 deletions
@@ -78,8 +78,8 @@ namespace osu.Game.Tests.Visual.SongSelectV2
{
base.SetUpSteps();
AddStep("load screen", () => Stack.Push(new Screens.SelectV2.SongSelectV2()));
AddUntilStep("wait for load", () => Stack.CurrentScreen is Screens.SelectV2.SongSelectV2 songSelect && songSelect.IsLoaded);
AddStep("load screen", () => Stack.Push(new Screens.SelectV2.SoloSongSelect()));
AddUntilStep("wait for load", () => Stack.CurrentScreen is Screens.SelectV2.SongSelect songSelect && songSelect.IsLoaded);
}
[Test]
@@ -16,7 +16,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2
base.SetUpSteps();
AddStep("press enter", () => InputManager.Key(Key.Enter));
AddWaitStep("wait", 5);
PushAndConfirm(() => new Screens.SelectV2.SongSelectV2());
PushAndConfirm(() => new Screens.SelectV2.SoloSongSelect());
}
[Test]
@@ -1030,7 +1030,10 @@ namespace osu.Game.Tests.Visual.UserInterface
private partial class TestModSelectOverlay : UserModSelectOverlay
{
protected override bool ShowPresets => true;
public TestModSelectOverlay()
{
ShowPresets = true;
}
}
private class TestUnimplementedMod : Mod
@@ -23,7 +23,7 @@ namespace osu.Game.Tests.Visual.UserInterface
{
private DependencyProvidingContainer contentContainer = null!;
private ScreenFooter screenFooter = null!;
private TestModSelectOverlay modOverlay = null!;
private UserModSelectOverlay modOverlay = null!;
[SetUp]
public void SetUp() => Schedule(() =>
@@ -39,7 +39,7 @@ namespace osu.Game.Tests.Visual.UserInterface
},
Children = new Drawable[]
{
modOverlay = new TestModSelectOverlay(),
modOverlay = new UserModSelectOverlay { ShowPresets = true },
new PopoverContainer
{
RelativeSizeAxes = Axes.Both,
@@ -196,11 +196,6 @@ namespace osu.Game.Tests.Visual.UserInterface
AddAssert("external overlay content still not shown", () => this.ChildrenOfType<TestShearedOverlayContainer.TestFooterContent>().SingleOrDefault()?.IsPresent, () => Is.Not.True);
}
private partial class TestModSelectOverlay : UserModSelectOverlay
{
protected override bool ShowPresets => true;
}
private partial class TestShearedOverlayContainer : ShearedOverlayContainer
{
public TestShearedOverlayContainer()
@@ -115,11 +115,10 @@ namespace osu.Game.Tests.Visual.UserInterface
private partial class TestModSelectOverlay : UserModSelectOverlay
{
protected override bool ShowPresets => true;
public TestModSelectOverlay()
: base(OverlayColourScheme.Aquamarine)
{
ShowPresets = true;
}
}
+3 -3
View File
@@ -35,7 +35,7 @@ using osuTK.Input;
namespace osu.Game.Overlays.Mods
{
public abstract partial class ModSelectOverlay : ShearedOverlayContainer, ISamplePlaybackDisabler, IKeyBindingHandler<PlatformAction>
public partial class ModSelectOverlay : ShearedOverlayContainer, ISamplePlaybackDisabler, IKeyBindingHandler<PlatformAction>
{
public const int BUTTON_WIDTH = 200;
@@ -96,7 +96,7 @@ namespace osu.Game.Overlays.Mods
/// <summary>
/// Whether the column with available mod presets should be shown.
/// </summary>
protected virtual bool ShowPresets => false;
public bool ShowPresets { get; init; }
protected virtual ModColumn CreateModColumn(ModType modType) => new ModColumn(modType, false);
@@ -125,7 +125,7 @@ namespace osu.Game.Overlays.Mods
[Resolved]
private ScreenFooter? footer { get; set; }
protected ModSelectOverlay(OverlayColourScheme colourScheme = OverlayColourScheme.Green)
public ModSelectOverlay(OverlayColourScheme colourScheme = OverlayColourScheme.Green)
: base(colourScheme)
{
}
+4 -6
View File
@@ -426,7 +426,10 @@ namespace osu.Game.Screens.Select
(beatmapOptionsButton = new FooterButtonOptions(), BeatmapOptions)
};
protected virtual ModSelectOverlay CreateModSelectOverlay() => new SoloModSelectOverlay();
protected virtual ModSelectOverlay CreateModSelectOverlay() => new UserModSelectOverlay
{
ShowPresets = true,
};
private DependencyContainer dependencies = null!;
@@ -1152,10 +1155,5 @@ namespace osu.Game.Screens.Select
return base.OnHover(e);
}
}
internal partial class SoloModSelectOverlay : UserModSelectOverlay
{
protected override bool ShowPresets => true;
}
}
}
@@ -10,6 +10,7 @@ using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Pooling;
using osu.Framework.Utils;
using osu.Game.Beatmaps;
using osu.Game.Database;
using osu.Game.Graphics.UserInterface;
@@ -20,6 +21,8 @@ namespace osu.Game.Screens.SelectV2
[Cached]
public partial class BeatmapCarousel : Carousel<BeatmapInfo>
{
public Action<BeatmapInfo>? RequestPresentBeatmap { private get; init; }
public const float SPACING = 5f;
private IBindableList<BeatmapSetInfo> detachedBeatmaps = null!;
@@ -128,6 +131,12 @@ namespace osu.Game.Screens.SelectV2
return;
case BeatmapInfo beatmapInfo:
if (ReferenceEquals(CurrentSelection, beatmapInfo))
{
RequestPresentBeatmap?.Invoke(beatmapInfo);
return;
}
CurrentSelection = beatmapInfo;
return;
}
@@ -252,6 +261,29 @@ namespace osu.Game.Screens.SelectV2
#endregion
#region Animation
/// <summary>
/// Moves non-selected beatmaps to the right, hiding off-screen.
/// </summary>
public bool VisuallyFocusSelected { get; set; }
private float selectionFocusOffset;
protected override void Update()
{
base.Update();
selectionFocusOffset = (float)Interpolation.DampContinuously(selectionFocusOffset, VisuallyFocusSelected ? 300 : 0, 100, Time.Elapsed);
}
protected override float GetPanelXOffset(Drawable panel)
{
return base.GetPanelXOffset(panel) + (((ICarouselPanel)panel).Selected.Value ? 0 : selectionFocusOffset);
}
#endregion
#region Filtering
public FilterCriteria Criteria { get; private set; } = new FilterCriteria();
+24 -19
View File
@@ -75,7 +75,7 @@ namespace osu.Game.Screens.SelectV2
/// <summary>
/// The number of items currently actualised into drawables.
/// </summary>
public int VisibleItems => scroll.Panels.Count;
public int VisibleItems => Scroll.Panels.Count;
/// <summary>
/// The currently selected model. Generally of type T.
@@ -185,7 +185,7 @@ namespace osu.Game.Screens.SelectV2
/// <param name="item">The item to find a related drawable representation.</param>
/// <returns>The drawable representation if it exists.</returns>
protected Drawable? GetMaterialisedDrawableForItem(CarouselItem item) =>
scroll.Panels.SingleOrDefault(p => ((ICarouselPanel)p).Item == item);
Scroll.Panels.SingleOrDefault(p => ((ICarouselPanel)p).Item == item);
/// <summary>
/// When a user is traversing the carousel via group selection keys, assert whether the item provided is a valid target.
@@ -222,11 +222,11 @@ namespace osu.Game.Screens.SelectV2
#region Initialisation
private readonly CarouselScrollContainer scroll;
protected readonly CarouselScrollContainer Scroll;
protected Carousel()
{
InternalChild = scroll = new CarouselScrollContainer
InternalChild = Scroll = new CarouselScrollContainer
{
RelativeSizeAxes = Axes.Both,
};
@@ -499,13 +499,13 @@ namespace osu.Game.Screens.SelectV2
// If a keyboard selection is currently made, we want to keep the view stable around the selection.
// That means that we should offset the immediate scroll position by any change in Y position for the selection.
if (prevKeyboard.YPosition != null && currentKeyboardSelection.YPosition != prevKeyboard.YPosition)
scroll.OffsetScrollPosition((float)(currentKeyboardSelection.YPosition!.Value - prevKeyboard.YPosition.Value));
Scroll.OffsetScrollPosition((float)(currentKeyboardSelection.YPosition!.Value - prevKeyboard.YPosition.Value));
}
private void scrollToSelection()
{
if (currentKeyboardSelection.CarouselItem != null)
scroll.ScrollTo(currentKeyboardSelection.CarouselItem.CarouselYPosition - visibleHalfHeight);
Scroll.ScrollTo(currentKeyboardSelection.CarouselItem.CarouselYPosition - visibleHalfHeight);
}
#endregion
@@ -519,12 +519,12 @@ namespace osu.Game.Screens.SelectV2
/// <summary>
/// The position of the lower visible bound with respect to the current scroll position.
/// </summary>
private float visibleBottomBound => (float)(scroll.Current + DrawHeight + BleedBottom);
private float visibleBottomBound => (float)(Scroll.Current + DrawHeight + BleedBottom);
/// <summary>
/// The position of the upper visible bound with respect to the current scroll position.
/// </summary>
private float visibleUpperBound => (float)(scroll.Current - BleedTop);
private float visibleUpperBound => (float)(Scroll.Current - BleedTop);
/// <summary>
/// Half the height of the visible content.
@@ -557,7 +557,7 @@ namespace osu.Game.Screens.SelectV2
double selectedYPos = currentSelection.CarouselItem?.CarouselYPosition ?? 0;
foreach (var panel in scroll.Panels)
foreach (var panel in Scroll.Panels)
{
var c = (ICarouselPanel)panel;
@@ -566,15 +566,12 @@ namespace osu.Game.Screens.SelectV2
continue;
float normalisedDepth = (float)(Math.Abs(selectedYPos - c.DrawYPosition) / DrawHeight);
scroll.Panels.ChangeChildDepth(panel, c.Item.DepthLayer + normalisedDepth);
Scroll.Panels.ChangeChildDepth(panel, c.Item.DepthLayer + normalisedDepth);
if (c.DrawYPosition != c.Item.CarouselYPosition)
c.DrawYPosition = Interpolation.DampContinuously(c.DrawYPosition, c.Item.CarouselYPosition, 50, Time.Elapsed);
Vector2 posInScroll = scroll.ToLocalSpace(panel.ScreenSpaceDrawQuad.Centre);
float dist = Math.Abs(1f - posInScroll.Y / visibleHalfHeight);
panel.X = offsetX(dist, visibleHalfHeight);
panel.X = GetPanelXOffset(panel);
c.Selected.Value = c.Item == currentSelection?.CarouselItem;
c.KeyboardSelected.Value = c.Item == currentKeyboardSelection?.CarouselItem;
@@ -582,6 +579,14 @@ namespace osu.Game.Screens.SelectV2
}
}
protected virtual float GetPanelXOffset(Drawable panel)
{
Vector2 posInScroll = Scroll.ToLocalSpace(panel.ScreenSpaceDrawQuad.Centre);
float dist = Math.Abs(1f - posInScroll.Y / visibleHalfHeight);
return offsetX(dist, visibleHalfHeight);
}
/// <summary>
/// Computes the x-offset of currently visible items. Makes the carousel appear round.
/// </summary>
@@ -628,7 +633,7 @@ namespace osu.Game.Screens.SelectV2
toDisplay.RemoveAll(i => !i.IsVisible);
// Iterate over all panels which are already displayed and figure which need to be displayed / removed.
foreach (var panel in scroll.Panels)
foreach (var panel in Scroll.Panels)
{
var carouselPanel = (ICarouselPanel)panel;
@@ -658,7 +663,7 @@ namespace osu.Game.Screens.SelectV2
carouselPanel.DrawYPosition = item.CarouselYPosition;
carouselPanel.Item = item;
scroll.Add(drawable);
Scroll.Add(drawable);
}
// Update the total height of all items (to make the scroll container scrollable through the full height even though
@@ -666,10 +671,10 @@ namespace osu.Game.Screens.SelectV2
if (carouselItems.Count > 0)
{
var lastItem = carouselItems[^1];
scroll.SetLayoutHeight((float)(lastItem.CarouselYPosition + lastItem.DrawHeight + visibleHalfHeight));
Scroll.SetLayoutHeight((float)(lastItem.CarouselYPosition + lastItem.DrawHeight + visibleHalfHeight));
}
else
scroll.SetLayoutHeight(0);
Scroll.SetLayoutHeight(0);
}
private static void expirePanelImmediately(Drawable panel)
@@ -713,7 +718,7 @@ namespace osu.Game.Screens.SelectV2
/// Implementation of scroll container which handles very large vertical lists by internally using <c>double</c> precision
/// for pre-display Y values.
/// </summary>
private partial class CarouselScrollContainer : UserTrackingScrollContainer, IKeyBindingHandler<GlobalAction>
protected partial class CarouselScrollContainer : UserTrackingScrollContainer, IKeyBindingHandler<GlobalAction>
{
public readonly Container Panels;
@@ -0,0 +1,28 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using osu.Framework.Screens;
using osu.Game.Screens.Play;
namespace osu.Game.Screens.SelectV2
{
public partial class SoloSongSelect : SongSelect
{
protected override bool OnStart()
{
this.Push(new PlayerLoaderV2(() => new SoloPlayer()));
return false;
}
private partial class PlayerLoaderV2 : PlayerLoader
{
public override bool ShowFooter => true;
public PlayerLoaderV2(Func<Player> createPlayer)
: base(createPlayer)
{
}
}
}
}
@@ -1,7 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
@@ -11,7 +10,7 @@ using osu.Game.Overlays;
using osu.Game.Overlays.Mods;
using osu.Game.Screens.Footer;
using osu.Game.Screens.Menu;
using osu.Game.Screens.Play;
using osu.Game.Screens.Select;
using osu.Game.Screens.SelectV2.Footer;
namespace osu.Game.Screens.SelectV2
@@ -20,15 +19,20 @@ namespace osu.Game.Screens.SelectV2
/// This screen is intended to house all components introduced in the new song select design to add transitions and examine the overall look.
/// This will be gradually built upon and ultimately replace <see cref="Select.SongSelect"/> once everything is in place.
/// </summary>
public partial class SongSelectV2 : OsuScreen
public abstract partial class SongSelect : OsuScreen
{
private const float logo_scale = 0.4f;
private readonly ModSelectOverlay modSelectOverlay = new SoloModSelectOverlay();
private readonly ModSelectOverlay modSelectOverlay = new ModSelectOverlay
{
ShowPresets = true,
};
[Cached]
private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Aquamarine);
private BeatmapCarousel carousel = null!;
public override bool ShowFooter => true;
[Resolved]
@@ -58,8 +62,9 @@ namespace osu.Game.Screens.SelectV2
{
RelativeSizeAxes = Axes.Both,
Padding = new MarginPadding { Bottom = ScreenFooter.HEIGHT },
Child = new BeatmapCarousel
Child = carousel = new BeatmapCarousel
{
RequestPresentBeatmap = _ => OnStart(),
RelativeSizeAxes = Axes.Both
},
},
@@ -97,9 +102,13 @@ namespace osu.Game.Screens.SelectV2
base.OnEntering(e);
}
private const double fade_duration = 300;
public override void OnResuming(ScreenTransitionEvent e)
{
this.FadeIn();
this.FadeIn(fade_duration, Easing.OutQuint);
carousel.VisuallyFocusSelected = false;
// required due to https://github.com/ppy/osu-framework/issues/3218
modSelectOverlay.SelectedMods.Disabled = false;
@@ -110,16 +119,18 @@ namespace osu.Game.Screens.SelectV2
public override void OnSuspending(ScreenTransitionEvent e)
{
this.Delay(400).FadeOut();
this.Delay(100).FadeOut(fade_duration, Easing.OutQuint);
modSelectOverlay.SelectedMods.UnbindFrom(Mods);
carousel.VisuallyFocusSelected = true;
base.OnSuspending(e);
}
public override bool OnExiting(ScreenExitEvent e)
{
this.Delay(400).FadeOut();
this.FadeOut(fade_duration, Easing.OutQuint);
return base.OnExiting(e);
}
@@ -141,11 +152,17 @@ namespace osu.Game.Screens.SelectV2
logo.Action = () =>
{
this.Push(new PlayerLoaderV2(() => new SoloPlayer()));
OnStart();
return false;
};
}
/// <summary>
/// Called when a selection is made.
/// </summary>
/// <returns>If a resultant action occurred that takes the user away from SongSelect.</returns>
protected abstract bool OnStart();
protected override void LogoSuspending(OsuLogo logo)
{
base.LogoSuspending(logo);
@@ -160,19 +177,17 @@ namespace osu.Game.Screens.SelectV2
logo.FadeOut(120, Easing.Out);
}
private partial class SoloModSelectOverlay : UserModSelectOverlay
/// <summary>
/// Set the query to the search text box.
/// </summary>
/// <param name="query">The string to search.</param>
public void Search(string query)
{
protected override bool ShowPresets => true;
}
private partial class PlayerLoaderV2 : PlayerLoader
{
public override bool ShowFooter => true;
public PlayerLoaderV2(Func<Player> createPlayer)
: base(createPlayer)
carousel.Filter(new FilterCriteria
{
}
// TODO: this should only set the text of the current criteria, not use a completely new criteria.
SearchText = query,
});
}
}
}
@@ -658,11 +658,10 @@ namespace osu.Game.Tests.Visual.Gameplay
private partial class TestModSelectOverlay : UserModSelectOverlay
{
protected override bool ShowPresets => false;
public TestModSelectOverlay()
: base(OverlayColourScheme.Aquamarine)
{
ShowPresets = false;
}
}
}