diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneFreeModSelectOverlay.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneFreeModSelectOverlay.cs index 938ab1e9f4..497faa28d0 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneFreeModSelectOverlay.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneFreeModSelectOverlay.cs @@ -3,6 +3,7 @@ #nullable disable +using System; using System.Collections.Generic; using System.Linq; using NUnit.Framework; @@ -16,6 +17,7 @@ using osu.Framework.Testing; using osu.Game.Overlays.Mods; using osu.Game.Rulesets.Osu.Mods; using osu.Game.Rulesets.Mods; +using osu.Game.Screens.Footer; using osu.Game.Screens.OnlinePlay; using osu.Game.Utils; using osuTK.Input; @@ -26,6 +28,7 @@ namespace osu.Game.Tests.Visual.Multiplayer { private FreeModSelectOverlay freeModSelectOverlay; private FooterButtonFreeMods footerButtonFreeMods; + private ScreenFooter footer; private readonly Bindable>> availableMods = new Bindable>>(); [BackgroundDependencyLoader] @@ -127,7 +130,7 @@ namespace osu.Game.Tests.Visual.Multiplayer { createFreeModSelect(); - AddAssert("overlay select all button enabled", () => freeModSelectOverlay.ChildrenOfType().Single().Enabled.Value); + AddAssert("overlay select all button enabled", () => this.ChildrenOfType().Single().Enabled.Value); AddAssert("footer button displays off", () => footerButtonFreeMods.ChildrenOfType().Any(t => t.Text == "off")); AddStep("click footer select all button", () => @@ -150,19 +153,27 @@ namespace osu.Game.Tests.Visual.Multiplayer private void createFreeModSelect() { - AddStep("create free mod select screen", () => Children = new Drawable[] + AddStep("create free mod select screen", () => Child = new DependencyProvidingContainer { - freeModSelectOverlay = new FreeModSelectOverlay + RelativeSizeAxes = Axes.Both, + Children = new Drawable[] { - State = { Value = Visibility.Visible } - }, - footerButtonFreeMods = new FooterButtonFreeMods(freeModSelectOverlay) - { - Anchor = Anchor.BottomRight, - Origin = Anchor.BottomRight, - Current = { BindTarget = freeModSelectOverlay.SelectedMods }, + freeModSelectOverlay = new FreeModSelectOverlay + { + State = { Value = Visibility.Visible } + }, + footerButtonFreeMods = new FooterButtonFreeMods(freeModSelectOverlay) + { + Anchor = Anchor.BottomRight, + Origin = Anchor.BottomRight, + Y = -ScreenFooter.HEIGHT, + Current = { BindTarget = freeModSelectOverlay.SelectedMods }, + }, + footer = new ScreenFooter(), }, + CachedDependencies = new (Type, object)[] { (typeof(ScreenFooter), footer) }, }); + AddUntilStep("all column content loaded", () => freeModSelectOverlay.ChildrenOfType().Any() && freeModSelectOverlay.ChildrenOfType().All(column => column.IsLoaded && column.ItemsLoaded)); diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs index f9ef085838..e2593e68e5 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs @@ -312,14 +312,14 @@ namespace osu.Game.Tests.Visual.Multiplayer AddUntilStep("wait for join", () => RoomJoined); ClickButtonWhenEnabled(); - AddAssert("mod select shows unranked", () => screen.UserModsSelectOverlay.ChildrenOfType().Single().Ranked.Value == false); - AddAssert("score multiplier = 1.20", () => screen.UserModsSelectOverlay.ChildrenOfType().Single().ModMultiplier.Value, () => Is.EqualTo(1.2).Within(0.01)); + AddAssert("mod select shows unranked", () => this.ChildrenOfType().Single().Ranked.Value == false); + AddAssert("score multiplier = 1.20", () => this.ChildrenOfType().Single().ModMultiplier.Value, () => Is.EqualTo(1.2).Within(0.01)); AddStep("select flashlight", () => screen.UserModsSelectOverlay.ChildrenOfType().Single(m => m.Mod is ModFlashlight).TriggerClick()); - AddAssert("score multiplier = 1.35", () => screen.UserModsSelectOverlay.ChildrenOfType().Single().ModMultiplier.Value, () => Is.EqualTo(1.35).Within(0.01)); + AddAssert("score multiplier = 1.35", () => this.ChildrenOfType().Single().ModMultiplier.Value, () => Is.EqualTo(1.35).Within(0.01)); AddStep("change flashlight setting", () => ((OsuModFlashlight)screen.UserModsSelectOverlay.SelectedMods.Value.Single()).FollowDelay.Value = 1200); - AddAssert("score multiplier = 1.20", () => screen.UserModsSelectOverlay.ChildrenOfType().Single().ModMultiplier.Value, () => Is.EqualTo(1.2).Within(0.01)); + AddAssert("score multiplier = 1.20", () => this.ChildrenOfType().Single().ModMultiplier.Value, () => Is.EqualTo(1.2).Within(0.01)); } private partial class TestMultiplayerMatchSubScreen : MultiplayerMatchSubScreen diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneSongSelectV2.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneSongSelectV2.cs index 674eaa2ff8..0a632793cc 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneSongSelectV2.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneSongSelectV2.cs @@ -181,12 +181,6 @@ namespace osu.Game.Tests.Visual.SongSelect #endregion - protected override void Update() - { - base.Update(); - Stack.Padding = new MarginPadding { Bottom = screenScreenFooter.DrawHeight - screenScreenFooter.Y }; - } - private void updateFooter(IScreen? _, IScreen? newScreen) { if (newScreen is IOsuScreen osuScreen && osuScreen.ShowFooter) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneFirstRunSetupOverlay.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneFirstRunSetupOverlay.cs index 51da4d8755..2ca06bf2f4 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneFirstRunSetupOverlay.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneFirstRunSetupOverlay.cs @@ -11,6 +11,8 @@ using Moq; using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Bindables; +using osu.Framework.Extensions.ObjectExtensions; +using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Screens; using osu.Framework.Testing; @@ -20,6 +22,7 @@ using osu.Game.Overlays; using osu.Game.Overlays.FirstRunSetup; using osu.Game.Overlays.Notifications; using osu.Game.Screens; +using osu.Game.Screens.Footer; using osuTK; using osuTK.Input; @@ -28,6 +31,7 @@ namespace osu.Game.Tests.Visual.UserInterface public partial class TestSceneFirstRunSetupOverlay : OsuManualInputManagerTestScene { private FirstRunSetupOverlay overlay; + private ScreenFooter footer; private readonly Mock performer = new Mock(); @@ -60,19 +64,16 @@ namespace osu.Game.Tests.Visual.UserInterface .Callback((Notification n) => lastNotification = n); }); - AddStep("add overlay", () => - { - Child = overlay = new FirstRunSetupOverlay - { - State = { Value = Visibility.Visible } - }; - }); + createOverlay(); + + AddStep("show overlay", () => overlay.Show()); } [Test] public void TestBasic() { AddAssert("overlay visible", () => overlay.State.Value == Visibility.Visible); + AddAssert("footer visible", () => footer.State.Value == Visibility.Visible); } [Test] @@ -82,16 +83,13 @@ namespace osu.Game.Tests.Visual.UserInterface AddUntilStep("step through", () => { - if (overlay.CurrentScreen?.IsLoaded != false) overlay.NextButton.TriggerClick(); + if (overlay.CurrentScreen?.IsLoaded != false) overlay.NextButton.AsNonNull().TriggerClick(); return overlay.State.Value == Visibility.Hidden; }); AddAssert("first run false", () => !LocalConfig.Get(OsuSetting.ShowFirstRunSetup)); - AddStep("add overlay", () => - { - Child = overlay = new FirstRunSetupOverlay(); - }); + createOverlay(); AddWaitStep("wait some", 5); @@ -109,7 +107,7 @@ namespace osu.Game.Tests.Visual.UserInterface if (keyboard) InputManager.Key(Key.Enter); else - overlay.NextButton.TriggerClick(); + overlay.NextButton.AsNonNull().TriggerClick(); } return overlay.State.Value == Visibility.Hidden; @@ -128,11 +126,9 @@ namespace osu.Game.Tests.Visual.UserInterface [TestCase(true)] public void TestBackButton(bool keyboard) { - AddAssert("back button disabled", () => !overlay.BackButton.Enabled.Value); - AddUntilStep("step to last", () => { - var nextButton = overlay.NextButton; + var nextButton = overlay.NextButton.AsNonNull(); if (overlay.CurrentScreen?.IsLoaded != false) nextButton.TriggerClick(); @@ -142,24 +138,29 @@ namespace osu.Game.Tests.Visual.UserInterface AddUntilStep("step back to start", () => { - if (overlay.CurrentScreen?.IsLoaded != false) + if (overlay.CurrentScreen?.IsLoaded != false && !(overlay.CurrentScreen is ScreenWelcome)) { if (keyboard) InputManager.Key(Key.Escape); else - overlay.BackButton.TriggerClick(); + footer.BackButton.TriggerClick(); } return overlay.CurrentScreen is ScreenWelcome; }); - AddAssert("back button disabled", () => !overlay.BackButton.Enabled.Value); + AddAssert("overlay not dismissed", () => overlay.State.Value == Visibility.Visible); if (keyboard) { AddStep("exit via keyboard", () => InputManager.Key(Key.Escape)); AddAssert("overlay dismissed", () => overlay.State.Value == Visibility.Hidden); } + else + { + AddStep("press back button", () => footer.BackButton.TriggerClick()); + AddAssert("overlay dismissed", () => overlay.State.Value == Visibility.Hidden); + } } [Test] @@ -185,7 +186,7 @@ namespace osu.Game.Tests.Visual.UserInterface [Test] public void TestResumeViaNotification() { - AddStep("step to next", () => overlay.NextButton.TriggerClick()); + AddStep("step to next", () => overlay.NextButton.AsNonNull().TriggerClick()); AddAssert("is at known screen", () => overlay.CurrentScreen is ScreenUIScale); @@ -200,6 +201,27 @@ namespace osu.Game.Tests.Visual.UserInterface AddAssert("is resumed", () => overlay.CurrentScreen is ScreenUIScale); } + private void createOverlay() + { + AddStep("add overlay", () => + { + var receptor = new ScreenFooter.BackReceptor(); + footer = new ScreenFooter(receptor); + + Child = new DependencyProvidingContainer + { + RelativeSizeAxes = Axes.Both, + CachedDependencies = new[] { (typeof(ScreenFooter), (object)footer) }, + Children = new Drawable[] + { + receptor, + overlay = new FirstRunSetupOverlay(), + footer, + } + }; + }); + } + // interface mocks break hot reload, mocking this stub implementation instead works around it. // see: https://github.com/moq/moq4/issues/1252 [UsedImplicitly] diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs index 21a5e3082b..e4622ffcf9 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs @@ -24,6 +24,7 @@ using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu.Mods; using osu.Game.Rulesets.Taiko.Mods; +using osu.Game.Screens.Footer; using osu.Game.Tests.Mods; using osuTK; using osuTK.Input; @@ -94,12 +95,28 @@ namespace osu.Game.Tests.Visual.UserInterface private void createScreen() { - AddStep("create screen", () => Child = modSelectOverlay = new TestModSelectOverlay + AddStep("create screen", () => { - RelativeSizeAxes = Axes.Both, - State = { Value = Visibility.Visible }, - Beatmap = Beatmap.Value, - SelectedMods = { BindTarget = SelectedMods } + var receptor = new ScreenFooter.BackReceptor(); + var footer = new ScreenFooter(receptor); + + Child = new DependencyProvidingContainer + { + RelativeSizeAxes = Axes.Both, + CachedDependencies = new[] { (typeof(ScreenFooter), (object)footer) }, + Children = new Drawable[] + { + receptor, + modSelectOverlay = new TestModSelectOverlay + { + RelativeSizeAxes = Axes.Both, + State = { Value = Visibility.Visible }, + Beatmap = { Value = Beatmap.Value }, + SelectedMods = { BindTarget = SelectedMods }, + }, + footer, + } + }; }); waitForColumnLoad(); } @@ -120,7 +137,7 @@ namespace osu.Game.Tests.Visual.UserInterface AddAssert("mod multiplier correct", () => { double multiplier = SelectedMods.Value.Aggregate(1d, (m, mod) => m * mod.ScoreMultiplier); - return Precision.AlmostEquals(multiplier, modSelectOverlay.ChildrenOfType().Single().ModMultiplier.Value); + return Precision.AlmostEquals(multiplier, this.ChildrenOfType().Single().ModMultiplier.Value); }); assertCustomisationToggleState(disabled: false, active: false); AddAssert("setting items created", () => modSelectOverlay.ChildrenOfType().Any()); @@ -135,7 +152,7 @@ namespace osu.Game.Tests.Visual.UserInterface AddAssert("mod multiplier correct", () => { double multiplier = SelectedMods.Value.Aggregate(1d, (m, mod) => m * mod.ScoreMultiplier); - return Precision.AlmostEquals(multiplier, modSelectOverlay.ChildrenOfType().Single().ModMultiplier.Value); + return Precision.AlmostEquals(multiplier, this.ChildrenOfType().Single().ModMultiplier.Value); }); assertCustomisationToggleState(disabled: false, active: false); AddAssert("setting items created", () => modSelectOverlay.ChildrenOfType().Any()); @@ -757,7 +774,7 @@ namespace osu.Game.Tests.Visual.UserInterface AddStep("click back button", () => { - InputManager.MoveMouseTo(modSelectOverlay.BackButton); + InputManager.MoveMouseTo(this.ChildrenOfType().Single()); InputManager.Click(MouseButton.Left); }); AddAssert("mod select hidden", () => modSelectOverlay.State.Value == Visibility.Hidden); @@ -885,7 +902,7 @@ namespace osu.Game.Tests.Visual.UserInterface InputManager.Click(MouseButton.Left); }); AddAssert("difficulty multiplier display shows correct value", - () => modSelectOverlay.ChildrenOfType().Single().ModMultiplier.Value, () => Is.EqualTo(0.1).Within(Precision.DOUBLE_EPSILON)); + () => this.ChildrenOfType().Single().ModMultiplier.Value, () => Is.EqualTo(0.1).Within(Precision.DOUBLE_EPSILON)); // this is highly unorthodox in a test, but because the `ModSettingChangeTracker` machinery heavily leans on events and object disposal and re-creation, // it is instrumental in the reproduction of the failure scenario that this test is supposed to cover. @@ -895,7 +912,7 @@ namespace osu.Game.Tests.Visual.UserInterface AddStep("reset half time speed to default", () => modSelectOverlay.ChildrenOfType().Single() .ChildrenOfType>().Single().TriggerClick()); AddUntilStep("difficulty multiplier display shows correct value", - () => modSelectOverlay.ChildrenOfType().Single().ModMultiplier.Value, () => Is.EqualTo(0.3).Within(Precision.DOUBLE_EPSILON)); + () => this.ChildrenOfType().Single().ModMultiplier.Value, () => Is.EqualTo(0.3).Within(Precision.DOUBLE_EPSILON)); } [Test] @@ -1015,8 +1032,6 @@ namespace osu.Game.Tests.Visual.UserInterface private partial class TestModSelectOverlay : UserModSelectOverlay { protected override bool ShowPresets => true; - - public new ShearedButton BackButton => base.BackButton; } private class TestUnimplementedMod : Mod diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneScreenFooter.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneScreenFooter.cs index 70c3664b9a..a4cf8a276f 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneScreenFooter.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneScreenFooter.cs @@ -23,7 +23,7 @@ namespace osu.Game.Tests.Visual.UserInterface { private DependencyProvidingContainer contentContainer = null!; private ScreenFooter screenFooter = null!; - private TestModSelectOverlay overlay = null!; + private TestModSelectOverlay modOverlay = null!; [SetUp] public void SetUp() => Schedule(() => @@ -39,7 +39,7 @@ namespace osu.Game.Tests.Visual.UserInterface }, Children = new Drawable[] { - overlay = new TestModSelectOverlay(), + modOverlay = new TestModSelectOverlay(), new PopoverContainer { RelativeSizeAxes = Axes.Both, @@ -51,7 +51,7 @@ namespace osu.Game.Tests.Visual.UserInterface screenFooter.SetButtons(new ScreenFooterButton[] { - new ScreenFooterButtonMods(overlay) { Current = SelectedMods }, + new ScreenFooterButtonMods(modOverlay) { Current = SelectedMods }, new ScreenFooterButtonRandom(), new ScreenFooterButtonOptions(), }); @@ -178,6 +178,24 @@ namespace osu.Game.Tests.Visual.UserInterface AddAssert("footer hidden", () => screenFooter.State.Value == Visibility.Hidden); } + [Test] + public void TestLoadOverlayAfterFooterIsDisplayed() + { + TestShearedOverlayContainer externalOverlay = null!; + + AddStep("show mod overlay", () => modOverlay.Show()); + AddUntilStep("mod footer content shown", () => this.ChildrenOfType().SingleOrDefault()?.IsPresent, () => Is.True); + + AddStep("add external overlay", () => contentContainer.Add(externalOverlay = new TestShearedOverlayContainer())); + AddUntilStep("wait for load", () => externalOverlay.IsLoaded); + AddAssert("mod footer content still shown", () => this.ChildrenOfType().SingleOrDefault()?.IsPresent, () => Is.True); + AddAssert("external overlay content not shown", () => this.ChildrenOfType().SingleOrDefault()?.IsPresent, () => Is.Not.True); + + AddStep("hide mod overlay", () => modOverlay.Hide()); + AddUntilStep("mod footer content hidden", () => this.ChildrenOfType().SingleOrDefault()?.IsPresent, () => Is.Not.True); + AddAssert("external overlay content still not shown", () => this.ChildrenOfType().SingleOrDefault()?.IsPresent, () => Is.Not.True); + } + private partial class TestModSelectOverlay : UserModSelectOverlay { protected override bool ShowPresets => true; @@ -185,8 +203,6 @@ namespace osu.Game.Tests.Visual.UserInterface private partial class TestShearedOverlayContainer : ShearedOverlayContainer { - public override bool UseNewFooter => true; - public TestShearedOverlayContainer() : base(OverlayColourScheme.Orange) { @@ -212,7 +228,7 @@ namespace osu.Game.Tests.Visual.UserInterface return false; } - public override Drawable CreateFooterContent() => new TestFooterContent(); + public override VisibilityContainer CreateFooterContent() => new TestFooterContent(); public partial class TestFooterContent : VisibilityContainer { diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 2e75c64c6e..388a98d947 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -51,6 +51,7 @@ using osu.Game.Online.Chat; using osu.Game.Online.Rooms; using osu.Game.Overlays; using osu.Game.Overlays.BeatmapListing; +using osu.Game.Overlays.Mods; using osu.Game.Overlays.Music; using osu.Game.Overlays.Notifications; using osu.Game.Overlays.SkinEditor; @@ -83,7 +84,7 @@ namespace osu.Game public partial class OsuGame : OsuGameBase, IKeyBindingHandler, ILocalUserPlayInfo, IPerformFromScreenRunner, IOverlayManager, ILinkHandler { #if DEBUG - // Different port allows runnning release and debug builds alongside each other. + // Different port allows running release and debug builds alongside each other. public const int IPC_PORT = 44824; #else public const int IPC_PORT = 44823; @@ -132,6 +133,8 @@ namespace osu.Game private Container topMostOverlayContent; + private Container footerBasedOverlayContent; + protected ScalingContainer ScreenContainer { get; private set; } protected Container ScreenOffsetContainer { get; private set; } @@ -156,8 +159,6 @@ namespace osu.Game private float toolbarOffset => (Toolbar?.Position.Y ?? 0) + (Toolbar?.DrawHeight ?? 0); - private float screenFooterOffset => (ScreenFooter?.DrawHeight ?? 0) - (ScreenFooter?.Position.Y ?? 0); - private IdleTracker idleTracker; /// @@ -242,7 +243,11 @@ namespace osu.Game throw new ArgumentException($@"{overlayContainer} has already been registered via {nameof(IOverlayManager.RegisterBlockingOverlay)} once."); externalOverlays.Add(overlayContainer); - overlayContent.Add(overlayContainer); + + if (overlayContainer is ShearedOverlayContainer) + footerBasedOverlayContent.Add(overlayContainer); + else + overlayContent.Add(overlayContainer); if (overlayContainer is OsuFocusedOverlayContainer focusedOverlayContainer) focusedOverlays.Add(focusedOverlayContainer); @@ -934,7 +939,6 @@ namespace osu.Game return string.Join(" / ", combinations); }; - Container logoContainer; ScreenFooter.BackReceptor backReceptor; dependencies.CacheAs(idleTracker = new GameIdleTracker(6000)); @@ -948,6 +952,8 @@ namespace osu.Game Add(sessionIdleTracker); + Container logoContainer; + AddRange(new Drawable[] { new VolumeControlReceptor @@ -976,11 +982,19 @@ namespace osu.Game Origin = Anchor.BottomLeft, Action = () => ScreenFooter.OnBack?.Invoke(), }, + logoContainer = new Container { RelativeSizeAxes = Axes.Both }, + footerBasedOverlayContent = new Container + { + Depth = -1, + RelativeSizeAxes = Axes.Both, + }, new PopoverContainer { + Depth = -1, RelativeSizeAxes = Axes.Both, Child = ScreenFooter = new ScreenFooter(backReceptor) { + RequestLogoInFront = inFront => ScreenContainer.ChangeChildDepth(logoContainer, inFront ? float.MinValue : 0), OnBack = () => { if (!(ScreenStack.CurrentScreen is IOsuScreen currentScreen)) @@ -991,7 +1005,6 @@ namespace osu.Game } }, }, - logoContainer = new Container { RelativeSizeAxes = Axes.Both }, } }, } @@ -1025,7 +1038,7 @@ namespace osu.Game if (!IsDeployedBuild) { - dependencies.Cache(versionManager = new VersionManager { Depth = int.MinValue }); + dependencies.Cache(versionManager = new VersionManager()); loadComponentSingleFile(versionManager, ScreenContainer.Add); } @@ -1072,7 +1085,7 @@ namespace osu.Game loadComponentSingleFile(CreateUpdateManager(), Add, true); // overlay elements - loadComponentSingleFile(FirstRunOverlay = new FirstRunSetupOverlay(), overlayContent.Add, true); + loadComponentSingleFile(FirstRunOverlay = new FirstRunSetupOverlay(), footerBasedOverlayContent.Add, true); loadComponentSingleFile(new ManageCollectionsDialog(), overlayContent.Add, true); loadComponentSingleFile(beatmapListing = new BeatmapListingOverlay(), overlayContent.Add, true); loadComponentSingleFile(dashboard = new DashboardOverlay(), overlayContent.Add, true); @@ -1137,7 +1150,7 @@ namespace osu.Game } // ensure only one of these overlays are open at once. - var singleDisplayOverlays = new OverlayContainer[] { FirstRunOverlay, chatOverlay, news, dashboard, beatmapListing, changelogOverlay, rankingsOverlay, wikiOverlay }; + var singleDisplayOverlays = new OverlayContainer[] { chatOverlay, news, dashboard, beatmapListing, changelogOverlay, rankingsOverlay, wikiOverlay }; foreach (var overlay in singleDisplayOverlays) { @@ -1485,7 +1498,6 @@ namespace osu.Game ScreenOffsetContainer.Padding = new MarginPadding { Top = toolbarOffset }; overlayOffsetContainer.Padding = new MarginPadding { Top = toolbarOffset }; - ScreenStack.Padding = new MarginPadding { Bottom = screenFooterOffset }; float horizontalOffset = 0f; diff --git a/osu.Game/Overlays/FirstRunSetup/ScreenUIScale.cs b/osu.Game/Overlays/FirstRunSetup/ScreenUIScale.cs index 02f0ad9506..d0eefa55c5 100644 --- a/osu.Game/Overlays/FirstRunSetup/ScreenUIScale.cs +++ b/osu.Game/Overlays/FirstRunSetup/ScreenUIScale.cs @@ -23,6 +23,7 @@ using osu.Game.Overlays.Settings; using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; using osu.Game.Screens; +using osu.Game.Screens.Footer; using osu.Game.Screens.Menu; using osu.Game.Screens.Select; using osu.Game.Tests.Visual; @@ -153,6 +154,7 @@ namespace osu.Game.Overlays.FirstRunSetup OsuScreenStack stack; OsuLogo logo; + ScreenFooter footer; Padding = new MarginPadding(5); @@ -166,7 +168,8 @@ namespace osu.Game.Overlays.FirstRunSetup { RelativePositionAxes = Axes.Both, Position = new Vector2(0.5f), - }) + }), + (typeof(ScreenFooter), footer = new ScreenFooter()), }, RelativeSizeAxes = Axes.Both, Children = new Drawable[] @@ -178,7 +181,8 @@ namespace osu.Game.Overlays.FirstRunSetup Children = new Drawable[] { stack = new OsuScreenStack(), - logo + footer, + logo, }, }, } diff --git a/osu.Game/Overlays/FirstRunSetupOverlay.cs b/osu.Game/Overlays/FirstRunSetupOverlay.cs index f2fdaefbb4..1a302cf51d 100644 --- a/osu.Game/Overlays/FirstRunSetupOverlay.cs +++ b/osu.Game/Overlays/FirstRunSetupOverlay.cs @@ -26,6 +26,7 @@ using osu.Game.Overlays.FirstRunSetup; using osu.Game.Overlays.Mods; using osu.Game.Overlays.Notifications; using osu.Game.Screens; +using osu.Game.Screens.Footer; using osu.Game.Screens.Menu; namespace osu.Game.Overlays @@ -44,8 +45,7 @@ namespace osu.Game.Overlays private ScreenStack? stack; - public ShearedButton NextButton = null!; - public ShearedButton BackButton = null!; + public ShearedButton? NextButton => DisplayedFooterContent?.NextButton; private readonly Bindable showFirstRunSetup = new Bindable(); @@ -90,7 +90,7 @@ namespace osu.Game.Overlays Anchor = Anchor.Centre, Origin = Anchor.Centre, RelativeSizeAxes = Axes.Both, - Padding = new MarginPadding { Bottom = 20, }, + Padding = new MarginPadding { Bottom = 20 }, Child = new GridContainer { Anchor = Anchor.Centre, @@ -134,51 +134,6 @@ namespace osu.Game.Overlays } }, }); - - FooterContent.Add(new GridContainer - { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Margin = new MarginPadding { Vertical = PADDING }, - Anchor = Anchor.BottomLeft, - Origin = Anchor.BottomLeft, - ColumnDimensions = new[] - { - new Dimension(GridSizeMode.Absolute, 10), - new Dimension(GridSizeMode.AutoSize), - new Dimension(), - new Dimension(GridSizeMode.Absolute, 10), - }, - RowDimensions = new[] - { - new Dimension(GridSizeMode.AutoSize), - }, - Content = new[] - { - new[] - { - Empty(), - BackButton = new ShearedButton(300) - { - Text = CommonStrings.Back, - Action = showPreviousStep, - Enabled = { Value = false }, - DarkerColour = colours.Pink2, - LighterColour = colours.Pink1, - }, - NextButton = new ShearedButton(0) - { - RelativeSizeAxes = Axes.X, - Width = 1, - Text = FirstRunSetupOverlayStrings.GetStarted, - DarkerColour = ColourProvider.Colour2, - LighterColour = ColourProvider.Colour1, - Action = showNextStep - }, - Empty(), - }, - } - }); } protected override void LoadComplete() @@ -190,6 +145,36 @@ namespace osu.Game.Overlays if (showFirstRunSetup.Value) Show(); } + [Resolved] + private ScreenFooter footer { get; set; } = null!; + + public new FirstRunSetupFooterContent? DisplayedFooterContent => base.DisplayedFooterContent as FirstRunSetupFooterContent; + + public override VisibilityContainer CreateFooterContent() + { + var footerContent = new FirstRunSetupFooterContent + { + ShowNextStep = showNextStep, + }; + + footerContent.OnLoadComplete += _ => updateButtons(); + return footerContent; + } + + public override bool OnBackButton() + { + if (currentStepIndex == 0) + return false; + + Debug.Assert(stack != null); + + stack.CurrentScreen.Exit(); + currentStepIndex--; + + updateButtons(); + return true; + } + public override bool OnPressed(KeyBindingPressEvent e) { if (!e.Repeat) @@ -197,19 +182,12 @@ namespace osu.Game.Overlays switch (e.Action) { case GlobalAction.Select: - NextButton.TriggerClick(); + DisplayedFooterContent?.NextButton.TriggerClick(); return true; case GlobalAction.Back: - if (BackButton.Enabled.Value) - { - BackButton.TriggerClick(); - return true; - } - - // If back button is disabled, we are at the first step. - // The base call will handle dismissal of the overlay. - break; + footer.BackButton.TriggerClick(); + return false; } } @@ -279,19 +257,6 @@ namespace osu.Game.Overlays showNextStep(); } - private void showPreviousStep() - { - if (currentStepIndex == 0) - return; - - Debug.Assert(stack != null); - - stack.CurrentScreen.Exit(); - currentStepIndex--; - - updateButtons(); - } - private void showNextStep() { Debug.Assert(currentStepIndex != null); @@ -322,29 +287,61 @@ namespace osu.Game.Overlays updateButtons(); } - private void updateButtons() + private void updateButtons() => DisplayedFooterContent?.UpdateButtons(currentStepIndex, steps); + + public partial class FirstRunSetupFooterContent : VisibilityContainer { - BackButton.Enabled.Value = currentStepIndex > 0; - NextButton.Enabled.Value = currentStepIndex != null; + public ShearedButton NextButton { get; private set; } = null!; - if (currentStepIndex == null) - return; + public Action? ShowNextStep; - bool isFirstStep = currentStepIndex == 0; - bool isLastStep = currentStepIndex == steps.Count - 1; - - if (isFirstStep) + [BackgroundDependencyLoader] + private void load(OverlayColourProvider colourProvider) { - BackButton.Text = CommonStrings.Back; - NextButton.Text = FirstRunSetupOverlayStrings.GetStarted; + RelativeSizeAxes = Axes.Both; + + InternalChild = NextButton = new ShearedButton(0) + { + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + Margin = new MarginPadding { Right = 12f }, + RelativeSizeAxes = Axes.X, + Width = 1, + Text = FirstRunSetupOverlayStrings.GetStarted, + DarkerColour = colourProvider.Colour2, + LighterColour = colourProvider.Colour1, + Action = () => ShowNextStep?.Invoke(), + }; } - else - { - BackButton.Text = LocalisableString.Interpolate($@"{CommonStrings.Back} ({steps[currentStepIndex.Value - 1].GetLocalisableDescription()})"); - NextButton.Text = isLastStep - ? CommonStrings.Finish - : LocalisableString.Interpolate($@"{CommonStrings.Next} ({steps[currentStepIndex.Value + 1].GetLocalisableDescription()})"); + public void UpdateButtons(int? currentStep, IReadOnlyList steps) + { + NextButton.Enabled.Value = currentStep != null; + + if (currentStep == null) + return; + + bool isFirstStep = currentStep == 0; + bool isLastStep = currentStep == steps.Count - 1; + + if (isFirstStep) + NextButton.Text = FirstRunSetupOverlayStrings.GetStarted; + else + { + NextButton.Text = isLastStep + ? CommonStrings.Finish + : LocalisableString.Interpolate($@"{CommonStrings.Next} ({steps[currentStep.Value + 1].GetLocalisableDescription()})"); + } + } + + protected override void PopIn() + { + this.FadeIn(); + } + + protected override void PopOut() + { + this.Delay(400).FadeOut(); } } } diff --git a/osu.Game/Overlays/Mods/ModSelectFooterContent.cs b/osu.Game/Overlays/Mods/ModSelectFooterContent.cs new file mode 100644 index 0000000000..146b8e4ebe --- /dev/null +++ b/osu.Game/Overlays/Mods/ModSelectFooterContent.cs @@ -0,0 +1,177 @@ +// 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.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.Configuration; +using osu.Game.Graphics.UserInterface; +using osu.Game.Rulesets.Mods; +using osuTK; + +namespace osu.Game.Overlays.Mods +{ + public partial class ModSelectFooterContent : VisibilityContainer + { + private readonly ModSelectOverlay overlay; + + private RankingInformationDisplay? rankingInformationDisplay; + private BeatmapAttributesDisplay? beatmapAttributesDisplay; + private FillFlowContainer buttonFlow = null!; + private FillFlowContainer contentFlow = null!; + + public DeselectAllModsButton? DeselectAllModsButton { get; set; } + + public readonly IBindable Beatmap = new Bindable(); + public readonly IBindable> ActiveMods = new Bindable>(); + + /// + /// Whether the effects (on score multiplier, on or beatmap difficulty) of the current selected set of mods should be shown. + /// + protected virtual bool ShowModEffects => true; + + /// + /// Whether the ranking information and beatmap attributes displays are stacked vertically due to small space. + /// + public bool DisplaysStackedVertically { get; private set; } + + public ModSelectFooterContent(ModSelectOverlay overlay) + { + this.overlay = overlay; + } + + [BackgroundDependencyLoader] + private void load() + { + RelativeSizeAxes = Axes.Both; + + InternalChild = buttonFlow = new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Horizontal, + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + Padding = new MarginPadding { Horizontal = 20 }, + Spacing = new Vector2(10), + ChildrenEnumerable = CreateButtons(), + }; + + if (ShowModEffects) + { + AddInternal(contentFlow = new FillFlowContainer + { + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Vertical, + Spacing = new Vector2(30, 10), + Anchor = Anchor.BottomRight, + Origin = Anchor.BottomRight, + Margin = new MarginPadding { Horizontal = 20 }, + Children = new Drawable[] + { + rankingInformationDisplay = new RankingInformationDisplay + { + Anchor = Anchor.BottomRight, + Origin = Anchor.BottomRight + }, + beatmapAttributesDisplay = new BeatmapAttributesDisplay + { + Anchor = Anchor.BottomRight, + Origin = Anchor.BottomRight, + BeatmapInfo = { Value = Beatmap.Value?.BeatmapInfo }, + }, + } + }); + } + } + + private ModSettingChangeTracker? modSettingChangeTracker; + + protected override void LoadComplete() + { + base.LoadComplete(); + + Beatmap.BindValueChanged(b => + { + if (beatmapAttributesDisplay != null) + beatmapAttributesDisplay.BeatmapInfo.Value = b.NewValue?.BeatmapInfo; + }, true); + + ActiveMods.BindValueChanged(m => + { + updateInformation(); + + modSettingChangeTracker?.Dispose(); + + // Importantly, use ActiveMods.Value here (and not the ValueChanged NewValue) as the latter can + // potentially be stale, due to complexities in the way change trackers work. + // + // See https://github.com/ppy/osu/pull/23284#issuecomment-1529056988 + modSettingChangeTracker = new ModSettingChangeTracker(ActiveMods.Value); + modSettingChangeTracker.SettingChanged += _ => updateInformation(); + }, true); + } + + private void updateInformation() + { + if (rankingInformationDisplay != null) + { + double multiplier = 1.0; + + foreach (var mod in ActiveMods.Value) + multiplier *= mod.ScoreMultiplier; + + rankingInformationDisplay.ModMultiplier.Value = multiplier; + rankingInformationDisplay.Ranked.Value = ActiveMods.Value.All(m => m.Ranked); + } + + if (beatmapAttributesDisplay != null) + beatmapAttributesDisplay.Mods.Value = ActiveMods.Value; + } + + protected override void Update() + { + base.Update(); + + if (beatmapAttributesDisplay != null) + { + float rightEdgeOfLastButton = buttonFlow[^1].ScreenSpaceDrawQuad.TopRight.X; + + // this is cheating a bit; the 640 value is hardcoded based on how wide the expanded panel _generally_ is. + // due to the transition applied, the raw screenspace quad of the panel cannot be used, as it will trigger an ugly feedback cycle of expanding and collapsing. + float projectedLeftEdgeOfExpandedBeatmapAttributesDisplay = buttonFlow.ToScreenSpace(buttonFlow.DrawSize - new Vector2(640, 0)).X; + + DisplaysStackedVertically = rightEdgeOfLastButton > projectedLeftEdgeOfExpandedBeatmapAttributesDisplay; + + // only update preview panel's collapsed state after we are fully visible, to ensure all the buttons are where we expect them to be. + if (Alpha == 1) + beatmapAttributesDisplay.Collapsed.Value = DisplaysStackedVertically; + + contentFlow.LayoutDuration = 200; + contentFlow.LayoutEasing = Easing.OutQuint; + contentFlow.Direction = DisplaysStackedVertically ? FillDirection.Vertical : FillDirection.Horizontal; + } + } + + protected virtual IEnumerable CreateButtons() => new[] + { + DeselectAllModsButton = new DeselectAllModsButton(overlay) + }; + + protected override void PopIn() + { + this.MoveToY(0, 400, Easing.OutQuint) + .FadeIn(400, Easing.OutQuint); + } + + protected override void PopOut() + { + this.MoveToY(-20f, 200, Easing.OutQuint) + .FadeOut(200, Easing.OutQuint); + } + } +} diff --git a/osu.Game/Overlays/Mods/ModSelectOverlay.cs b/osu.Game/Overlays/Mods/ModSelectOverlay.cs index d5a4d27237..7469590895 100644 --- a/osu.Game/Overlays/Mods/ModSelectOverlay.cs +++ b/osu.Game/Overlays/Mods/ModSelectOverlay.cs @@ -27,6 +27,7 @@ using osu.Game.Graphics.UserInterface; using osu.Game.Input.Bindings; using osu.Game.Localisation; using osu.Game.Rulesets.Mods; +using osu.Game.Screens.Footer; using osu.Game.Utils; using osuTK; using osuTK.Graphics; @@ -87,11 +88,6 @@ namespace osu.Game.Overlays.Mods public ShearedSearchTextBox SearchTextBox { get; private set; } = null!; - /// - /// Whether the effects (on score multiplier, on or beatmap difficulty) of the current selected set of mods should be shown. - /// - protected virtual bool ShowModEffects => true; - /// /// Whether per-mod customisation controls are visible. /// @@ -108,11 +104,6 @@ namespace osu.Game.Overlays.Mods protected virtual IReadOnlyList ComputeActiveMods() => SelectedMods.Value; - protected virtual IEnumerable CreateFooterButtons() - { - yield return deselectAllModsButton = new DeselectAllModsButton(this); - } - private readonly Bindable>> globalAvailableMods = new Bindable>>(); public IEnumerable AllAvailableMods => AvailableMods.Value.SelectMany(pair => pair.Value); @@ -121,34 +112,18 @@ namespace osu.Game.Overlays.Mods private ColumnScrollContainer columnScroll = null!; private ColumnFlowContainer columnFlow = null!; - private FillFlowContainer footerButtonFlow = null!; - private FillFlowContainer footerContentFlow = null!; - private DeselectAllModsButton deselectAllModsButton = null!; private Container aboveColumnsContent = null!; - private RankingInformationDisplay? rankingInformationDisplay; - private BeatmapAttributesDisplay? beatmapAttributesDisplay; private ModCustomisationPanel customisationPanel = null!; - protected ShearedButton BackButton { get; private set; } = null!; - protected SelectAllModsButton? SelectAllModsButton { get; set; } + protected virtual SelectAllModsButton? SelectAllModsButton => null; private Sample? columnAppearSample; - private WorkingBeatmap? beatmap; + public readonly Bindable Beatmap = new Bindable(); - public WorkingBeatmap? Beatmap - { - get => beatmap; - set - { - if (beatmap == value) return; - - beatmap = value; - if (IsLoaded && beatmapAttributesDisplay != null) - beatmapAttributesDisplay.BeatmapInfo.Value = beatmap?.BeatmapInfo; - } - } + [Resolved] + private ScreenFooter? footer { get; set; } protected ModSelectOverlay(OverlayColourScheme colourScheme = OverlayColourScheme.Green) : base(colourScheme) @@ -227,59 +202,6 @@ namespace osu.Game.Overlays.Mods } }); - FooterContent.Add(footerButtonFlow = new FillFlowContainer - { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Direction = FillDirection.Horizontal, - Anchor = Anchor.BottomLeft, - Origin = Anchor.BottomLeft, - Padding = new MarginPadding - { - Vertical = PADDING, - Horizontal = 70 - }, - Spacing = new Vector2(10), - ChildrenEnumerable = CreateFooterButtons().Prepend(BackButton = new ShearedButton(BUTTON_WIDTH) - { - Text = CommonStrings.Back, - Action = Hide, - DarkerColour = colours.Pink2, - LighterColour = colours.Pink1 - }) - }); - - if (ShowModEffects) - { - FooterContent.Add(footerContentFlow = new FillFlowContainer - { - AutoSizeAxes = Axes.Both, - Direction = FillDirection.Vertical, - Spacing = new Vector2(30, 10), - Anchor = Anchor.BottomRight, - Origin = Anchor.BottomRight, - Margin = new MarginPadding - { - Vertical = PADDING, - Horizontal = 20 - }, - Children = new Drawable[] - { - rankingInformationDisplay = new RankingInformationDisplay - { - Anchor = Anchor.BottomRight, - Origin = Anchor.BottomRight - }, - beatmapAttributesDisplay = new BeatmapAttributesDisplay - { - Anchor = Anchor.BottomRight, - Origin = Anchor.BottomRight, - BeatmapInfo = { Value = Beatmap?.BeatmapInfo }, - }, - } - }); - } - globalAvailableMods.BindTo(game.AvailableMods); textSearchStartsActive = configManager.GetBindable(OsuSetting.ModSelectTextSearchStartsActive); @@ -293,8 +215,6 @@ namespace osu.Game.Overlays.Mods SearchTextBox.Current.Value = string.Empty; } - private ModSettingChangeTracker? modSettingChangeTracker; - protected override void LoadComplete() { // this is called before base call so that the mod state is populated early, and the transition in `PopIn()` can play out properly. @@ -317,23 +237,6 @@ namespace osu.Game.Overlays.Mods ActiveMods.Value = ComputeActiveMods(); }, true); - ActiveMods.BindValueChanged(_ => - { - updateOverlayInformation(); - - modSettingChangeTracker?.Dispose(); - - if (AllowCustomisation) - { - // Importantly, use ActiveMods.Value here (and not the ValueChanged NewValue) as the latter can - // potentially be stale, due to complexities in the way change trackers work. - // - // See https://github.com/ppy/osu/pull/23284#issuecomment-1529056988 - modSettingChangeTracker = new ModSettingChangeTracker(ActiveMods.Value); - modSettingChangeTracker.SettingChanged += _ => updateOverlayInformation(); - } - }, true); - customisationPanel.Expanded.BindValueChanged(_ => updateCustomisationVisualState(), true); SearchTextBox.Current.BindValueChanged(query => @@ -351,6 +254,14 @@ namespace osu.Game.Overlays.Mods }); } + public new ModSelectFooterContent? DisplayedFooterContent => base.DisplayedFooterContent as ModSelectFooterContent; + + public override VisibilityContainer CreateFooterContent() => new ModSelectFooterContent(this) + { + Beatmap = { BindTarget = Beatmap }, + ActiveMods = { BindTarget = ActiveMods }, + }; + private static readonly LocalisableString input_search_placeholder = Resources.Localisation.Web.CommonStrings.InputSearch; private static readonly LocalisableString tab_to_search_placeholder = ModSelectOverlayStrings.TabToSearch; @@ -359,26 +270,7 @@ namespace osu.Game.Overlays.Mods base.Update(); SearchTextBox.PlaceholderText = SearchTextBox.HasFocus ? input_search_placeholder : tab_to_search_placeholder; - - if (beatmapAttributesDisplay != null) - { - float rightEdgeOfLastButton = footerButtonFlow[^1].ScreenSpaceDrawQuad.TopRight.X; - - // this is cheating a bit; the 640 value is hardcoded based on how wide the expanded panel _generally_ is. - // due to the transition applied, the raw screenspace quad of the panel cannot be used, as it will trigger an ugly feedback cycle of expanding and collapsing. - float projectedLeftEdgeOfExpandedBeatmapAttributesDisplay = footerButtonFlow.ToScreenSpace(footerButtonFlow.DrawSize - new Vector2(640, 0)).X; - - bool screenIsntWideEnough = rightEdgeOfLastButton > projectedLeftEdgeOfExpandedBeatmapAttributesDisplay; - - // only update preview panel's collapsed state after we are fully visible, to ensure all the buttons are where we expect them to be. - if (Alpha == 1) - beatmapAttributesDisplay.Collapsed.Value = screenIsntWideEnough; - - footerContentFlow.LayoutDuration = 200; - footerContentFlow.LayoutEasing = Easing.OutQuint; - footerContentFlow.Direction = screenIsntWideEnough ? FillDirection.Vertical : FillDirection.Horizontal; - aboveColumnsContent.Padding = aboveColumnsContent.Padding with { Bottom = screenIsntWideEnough ? 70f : 15f }; - } + aboveColumnsContent.Padding = aboveColumnsContent.Padding with { Bottom = DisplayedFooterContent?.DisplaysStackedVertically == true ? 75f : 15f }; } /// @@ -456,27 +348,6 @@ namespace osu.Game.Overlays.Mods modState.ValidForSelection.Value = modState.Mod.Type != ModType.System && modState.Mod.HasImplementation && IsValidMod.Invoke(modState.Mod); } - /// - /// Updates any information displayed on the overlay regarding the effects of the active mods. - /// This reads from instead of . - /// - private void updateOverlayInformation() - { - if (rankingInformationDisplay != null) - { - double multiplier = 1.0; - - foreach (var mod in ActiveMods.Value) - multiplier *= mod.ScoreMultiplier; - - rankingInformationDisplay.ModMultiplier.Value = multiplier; - rankingInformationDisplay.Ranked.Value = ActiveMods.Value.All(m => m.Ranked); - } - - if (beatmapAttributesDisplay != null) - beatmapAttributesDisplay.Mods.Value = ActiveMods.Value; - } - private void updateCustomisation() { if (!AllowCustomisation) @@ -702,7 +573,7 @@ namespace osu.Game.Overlays.Mods { if (!SearchTextBox.HasFocus && !customisationPanel.Expanded.Value) { - deselectAllModsButton.TriggerClick(); + DisplayedFooterContent?.DeselectAllModsButton?.TriggerClick(); return true; } @@ -733,7 +604,13 @@ namespace osu.Game.Overlays.Mods return base.OnPressed(e); - void hideOverlay() => BackButton.TriggerClick(); + void hideOverlay() + { + if (footer != null) + footer.BackButton.TriggerClick(); + else + Hide(); + } } /// @@ -741,7 +618,7 @@ namespace osu.Game.Overlays.Mods /// This is handled locally here due to conflicts in input handling between the search text box and the select all mods button. /// Attempting to handle this action locally in both places leads to a possible scenario /// wherein activating the "select all" platform binding will both select all text in the search box and select all mods. - /// > + /// public bool OnPressed(KeyBindingPressEvent e) { if (e.Repeat || e.Action != PlatformAction.SelectAll || SelectAllModsButton == null) diff --git a/osu.Game/Overlays/Mods/ShearedOverlayContainer.cs b/osu.Game/Overlays/Mods/ShearedOverlayContainer.cs index aed9b395f6..dfa49f3779 100644 --- a/osu.Game/Overlays/Mods/ShearedOverlayContainer.cs +++ b/osu.Game/Overlays/Mods/ShearedOverlayContainer.cs @@ -1,12 +1,13 @@ // 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 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.Input.Events; -using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Graphics.UserInterface; using osu.Game.Screens.Footer; @@ -37,9 +38,6 @@ namespace osu.Game.Overlays.Mods [Resolved] private ScreenFooter? footer { get; set; } - // todo: very temporary property that will be removed once ModSelectOverlay and FirstRunSetupOverlay are updated to use new footer. - public virtual bool UseNewFooter => false; - /// /// A container containing all content, including the header and footer. /// May be used for overlay-wide animations. @@ -60,6 +58,10 @@ namespace osu.Game.Overlays.Mods protected override bool BlockNonPositionalInput => true; + // ShearedOverlayContainers are placed at a layer within the screen container as they rely on ScreenFooter which must be placed there. + // Therefore, dimming must be managed locally, since DimMainContent dims the entire screen layer. + protected sealed override bool DimMainContent => false; + protected ShearedOverlayContainer(OverlayColourScheme colourScheme) { RelativeSizeAxes = Axes.Both; @@ -70,13 +72,16 @@ namespace osu.Game.Overlays.Mods [BackgroundDependencyLoader] private void load() { - const float footer_height = ScreenFooter.HEIGHT; - Child = TopLevelContent = new Container { RelativeSizeAxes = Axes.Both, Children = new Drawable[] { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = ColourProvider.Background6.Opacity(0.75f), + }, Header = new ShearedOverlayHeader { Anchor = Anchor.TopCentre, @@ -90,38 +95,19 @@ namespace osu.Game.Overlays.Mods Padding = new MarginPadding { Top = ShearedOverlayHeader.HEIGHT, - Bottom = footer_height + PADDING, + Bottom = ScreenFooter.HEIGHT + PADDING, } }, - Footer = new InputBlockingContainer - { - RelativeSizeAxes = Axes.X, - Depth = float.MinValue, - Height = footer_height, - Margin = new MarginPadding { Top = PADDING }, - Anchor = Anchor.BottomCentre, - Origin = Anchor.BottomCentre, - Children = new Drawable[] - { - new Box - { - RelativeSizeAxes = Axes.Both, - Colour = ColourProvider.Background5 - }, - FooterContent = new Container - { - RelativeSizeAxes = Axes.Both, - }, - } - } } }; } + public VisibilityContainer? DisplayedFooterContent { get; private set; } + /// /// Creates content to be displayed on the game-wide footer. /// - public virtual Drawable CreateFooterContent() => Empty(); + public virtual VisibilityContainer? CreateFooterContent() => null; /// /// Invoked when the back button in the footer is pressed. @@ -140,6 +126,7 @@ namespace osu.Game.Overlays.Mods return base.OnClick(e); } + private IDisposable? activeOverlayRegistration; private bool hideFooterOnPopOut; protected override void PopIn() @@ -150,9 +137,10 @@ namespace osu.Game.Overlays.Mods Header.MoveToY(0, fade_in_duration, Easing.OutQuint); - if (UseNewFooter && footer != null) + if (footer != null) { - footer.SetActiveOverlayContainer(this); + activeOverlayRegistration = footer.RegisterActiveOverlayContainer(this, out var footerContent); + DisplayedFooterContent = footerContent; if (footer.State.Value == Visibility.Hidden) { @@ -160,8 +148,6 @@ namespace osu.Game.Overlays.Mods hideFooterOnPopOut = true; } } - else - Footer.MoveToY(0, fade_in_duration, Easing.OutQuint); } protected override void PopOut() @@ -173,9 +159,11 @@ namespace osu.Game.Overlays.Mods Header.MoveToY(-Header.DrawHeight, fade_out_duration, Easing.OutQuint); - if (UseNewFooter && footer != null) + if (footer != null) { - footer.ClearActiveOverlayContainer(); + activeOverlayRegistration?.Dispose(); + activeOverlayRegistration = null; + DisplayedFooterContent = null; if (hideFooterOnPopOut) { @@ -183,8 +171,6 @@ namespace osu.Game.Overlays.Mods hideFooterOnPopOut = false; } } - else - Footer.MoveToY(Footer.DrawHeight, fade_out_duration, Easing.OutQuint); } } } diff --git a/osu.Game/Screens/Footer/ScreenFooter.cs b/osu.Game/Screens/Footer/ScreenFooter.cs index f9a6d54b96..6a1efcf87a 100644 --- a/osu.Game/Screens/Footer/ScreenFooter.cs +++ b/osu.Game/Screens/Footer/ScreenFooter.cs @@ -11,6 +11,7 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Framework.Input.Bindings; using osu.Framework.Input.Events; +using osu.Framework.Threading; using osu.Game.Graphics.Containers; using osu.Game.Input.Bindings; using osu.Game.Overlays; @@ -24,6 +25,7 @@ namespace osu.Game.Screens.Footer { private const int padding = 60; private const float delay_per_button = 30; + private const double transition_duration = 400; public const int HEIGHT = 50; @@ -37,8 +39,13 @@ namespace osu.Game.Screens.Footer [Cached] private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Aquamarine); + [Resolved] + private OsuGame? game { get; set; } + public ScreenBackButton BackButton { get; private set; } = null!; + public Action? RequestLogoInFront { get; set; } + public Action? OnBack; public ScreenFooter(BackReceptor? receptor = null) @@ -101,19 +108,35 @@ namespace osu.Game.Screens.Footer }; } - public void StartTrackingLogo(OsuLogo logo, float duration = 0, Easing easing = Easing.None) => logoTrackingContainer.StartTracking(logo, duration, easing); - public void StopTrackingLogo() => logoTrackingContainer.StopTracking(); + private ScheduledDelegate? changeLogoDepthDelegate; + + public void StartTrackingLogo(OsuLogo logo, float duration = 0, Easing easing = Easing.None) + { + changeLogoDepthDelegate?.Cancel(); + changeLogoDepthDelegate = null; + + logoTrackingContainer.StartTracking(logo, duration, easing); + RequestLogoInFront?.Invoke(true); + } + + public void StopTrackingLogo() + { + logoTrackingContainer.StopTracking(); + + if (game != null) + changeLogoDepthDelegate = Scheduler.AddDelayed(() => RequestLogoInFront?.Invoke(false), transition_duration); + } protected override void PopIn() { - this.MoveToY(0, 400, Easing.OutQuint) - .FadeIn(400, Easing.OutQuint); + this.MoveToY(0, transition_duration, Easing.OutQuint) + .FadeIn(transition_duration, Easing.OutQuint); } protected override void PopOut() { - this.MoveToY(HEIGHT, 400, Easing.OutQuint) - .FadeOut(400, Easing.OutQuint); + this.MoveToY(HEIGHT, transition_duration, Easing.OutQuint) + .FadeOut(transition_duration, Easing.OutQuint); } public void SetButtons(IReadOnlyList buttons) @@ -121,7 +144,7 @@ namespace osu.Game.Screens.Footer temporarilyHiddenButtons.Clear(); overlays.Clear(); - ClearActiveOverlayContainer(); + clearActiveOverlayContainer(); var oldButtons = buttonsFlow.ToArray(); @@ -166,14 +189,15 @@ namespace osu.Game.Screens.Footer private ShearedOverlayContainer? activeOverlay; private Container? contentContainer; + private readonly List temporarilyHiddenButtons = new List(); - public void SetActiveOverlayContainer(ShearedOverlayContainer overlay) + public IDisposable RegisterActiveOverlayContainer(ShearedOverlayContainer overlay, out VisibilityContainer? footerContent) { - if (contentContainer != null) + if (activeOverlay != null) { throw new InvalidOperationException(@"Cannot set overlay content while one is already present. " + - $@"The previous overlay whose content is {contentContainer.Child.GetType().Name} should be hidden first."); + $@"The previous overlay ({activeOverlay.GetType().Name}) should be hidden first."); } activeOverlay = overlay; @@ -197,7 +221,9 @@ namespace osu.Game.Screens.Footer updateColourScheme(overlay.ColourProvider.ColourScheme); - var content = overlay.CreateFooterContent(); + footerContent = overlay.CreateFooterContent(); + + var content = footerContent ?? Empty(); Add(contentContainer = new Container { @@ -211,29 +237,30 @@ namespace osu.Game.Screens.Footer this.Delay(60).Schedule(() => content.Show()); else content.Show(); + + return new InvokeOnDisposal(clearActiveOverlayContainer); } - public void ClearActiveOverlayContainer() + private void clearActiveOverlayContainer() { - if (contentContainer == null) + if (activeOverlay == null) return; + Debug.Assert(contentContainer != null); contentContainer.Child.Hide(); double timeUntilRun = contentContainer.Child.LatestTransformEndTime - Time.Current; - Container expireTarget = contentContainer; - contentContainer = null; - activeOverlay = null; - for (int i = 0; i < temporarilyHiddenButtons.Count; i++) makeButtonAppearFromBottom(temporarilyHiddenButtons[i], 0); temporarilyHiddenButtons.Clear(); - expireTarget.Delay(timeUntilRun).Expire(); - updateColourScheme(OverlayColourScheme.Aquamarine); + + contentContainer.Delay(timeUntilRun).Expire(); + contentContainer = null; + activeOverlay = null; } private void updateColourScheme(OverlayColourScheme colourScheme) diff --git a/osu.Game/Screens/OnlinePlay/FreeModSelectOverlay.cs b/osu.Game/Screens/OnlinePlay/FreeModSelectOverlay.cs index 7f090aca57..8937abb775 100644 --- a/osu.Game/Screens/OnlinePlay/FreeModSelectOverlay.cs +++ b/osu.Game/Screens/OnlinePlay/FreeModSelectOverlay.cs @@ -6,6 +6,7 @@ using osu.Game.Overlays; using System.Collections.Generic; using System.Linq; using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; using osu.Game.Graphics.UserInterface; using osu.Game.Overlays.Mods; using osu.Game.Rulesets.Mods; @@ -14,8 +15,6 @@ namespace osu.Game.Screens.OnlinePlay { public partial class FreeModSelectOverlay : ModSelectOverlay { - protected override bool ShowModEffects => false; - protected override bool AllowCustomisation => false; public new Func IsValidMod @@ -24,6 +23,8 @@ namespace osu.Game.Screens.OnlinePlay set => base.IsValidMod = m => m.UserPlayable && value.Invoke(m); } + protected override SelectAllModsButton? SelectAllModsButton => DisplayedFooterContent?.SelectAllModsButton; + public FreeModSelectOverlay() : base(OverlayColourScheme.Plum) { @@ -32,12 +33,35 @@ namespace osu.Game.Screens.OnlinePlay protected override ModColumn CreateModColumn(ModType modType) => new ModColumn(modType, true); - protected override IEnumerable CreateFooterButtons() - => base.CreateFooterButtons() - .Prepend(SelectAllModsButton = new SelectAllModsButton(this) - { - Anchor = Anchor.BottomLeft, - Origin = Anchor.BottomLeft, - }); + public new FreeModSelectFooterContent? DisplayedFooterContent => base.DisplayedFooterContent as FreeModSelectFooterContent; + + public override VisibilityContainer CreateFooterContent() => new FreeModSelectFooterContent(this) + { + Beatmap = { BindTarget = Beatmap }, + ActiveMods = { BindTarget = ActiveMods }, + }; + + public partial class FreeModSelectFooterContent : ModSelectFooterContent + { + private readonly FreeModSelectOverlay overlay; + + protected override bool ShowModEffects => false; + + public SelectAllModsButton? SelectAllModsButton; + + public FreeModSelectFooterContent(FreeModSelectOverlay overlay) + : base(overlay) + { + this.overlay = overlay; + } + + protected override IEnumerable CreateButtons() + => base.CreateButtons() + .Prepend(SelectAllModsButton = new SelectAllModsButton(overlay) + { + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + }); + } } } diff --git a/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs index 515d9fc7a5..7c8931c04e 100644 --- a/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs @@ -456,7 +456,7 @@ namespace osu.Game.Screens.OnlinePlay.Match // Retrieve the corresponding local beatmap, since we can't directly use the playlist's beatmap info var localBeatmap = beatmap == null ? null : beatmapManager.QueryBeatmap(b => b.OnlineID == beatmap.OnlineID); - UserModsSelectOverlay.Beatmap = Beatmap.Value = beatmapManager.GetWorkingBeatmap(localBeatmap); + UserModsSelectOverlay.Beatmap.Value = Beatmap.Value = beatmapManager.GetWorkingBeatmap(localBeatmap); } protected virtual void UpdateMods() diff --git a/osu.Game/Screens/Select/SongSelect.cs b/osu.Game/Screens/Select/SongSelect.cs index df7eabfd21..ecf8210002 100644 --- a/osu.Game/Screens/Select/SongSelect.cs +++ b/osu.Game/Screens/Select/SongSelect.cs @@ -851,7 +851,7 @@ namespace osu.Game.Screens.Select BeatmapDetails.Beatmap = beatmap; - ModSelect.Beatmap = beatmap; + ModSelect.Beatmap.Value = beatmap; advancedStats.BeatmapInfo = beatmap.BeatmapInfo; diff --git a/osu.Game/Screens/SelectV2/SongSelectV2.cs b/osu.Game/Screens/SelectV2/SongSelectV2.cs index 10ed7783c4..2f9667793f 100644 --- a/osu.Game/Screens/SelectV2/SongSelectV2.cs +++ b/osu.Game/Screens/SelectV2/SongSelectV2.cs @@ -6,7 +6,6 @@ using System.Collections.Generic; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Cursor; using osu.Framework.Screens; using osu.Game.Overlays; using osu.Game.Overlays.Mods; @@ -23,6 +22,8 @@ namespace osu.Game.Screens.SelectV2 /// public partial class SongSelectV2 : OsuScreen { + private const float logo_scale = 0.4f; + private readonly ModSelectOverlay modSelectOverlay = new SoloModSelectOverlay(); [Cached] @@ -30,15 +31,14 @@ namespace osu.Game.Screens.SelectV2 public override bool ShowFooter => true; + [Resolved] + private OsuLogo? logo { get; set; } + [BackgroundDependencyLoader] private void load() { AddRangeInternal(new Drawable[] { - new PopoverContainer - { - RelativeSizeAxes = Axes.Both, - }, modSelectOverlay, }); } @@ -50,21 +50,43 @@ namespace osu.Game.Screens.SelectV2 new ScreenFooterButtonOptions(), }; + protected override void LoadComplete() + { + base.LoadComplete(); + + modSelectOverlay.State.BindValueChanged(v => + { + logo?.ScaleTo(v.NewValue == Visibility.Visible ? 0f : logo_scale, 400, Easing.OutQuint) + .FadeTo(v.NewValue == Visibility.Visible ? 0f : 1f, 200, Easing.OutQuint); + }, true); + } + public override void OnEntering(ScreenTransitionEvent e) { this.FadeIn(); + + modSelectOverlay.SelectedMods.BindTo(Mods); + base.OnEntering(e); } public override void OnResuming(ScreenTransitionEvent e) { this.FadeIn(); + + // required due to https://github.com/ppy/osu-framework/issues/3218 + modSelectOverlay.SelectedMods.Disabled = false; + modSelectOverlay.SelectedMods.BindTo(Mods); + base.OnResuming(e); } public override void OnSuspending(ScreenTransitionEvent e) { this.Delay(400).FadeOut(); + + modSelectOverlay.SelectedMods.UnbindFrom(Mods); + base.OnSuspending(e); } @@ -74,17 +96,6 @@ namespace osu.Game.Screens.SelectV2 return base.OnExiting(e); } - public override bool OnBackButton() - { - if (modSelectOverlay.State.Value == Visibility.Visible) - { - modSelectOverlay.Hide(); - return true; - } - - return false; - } - protected override void LogoArriving(OsuLogo logo, bool resuming) { base.LogoArriving(logo, resuming); @@ -99,7 +110,7 @@ namespace osu.Game.Screens.SelectV2 } logo.FadeIn(240, Easing.OutQuint); - logo.ScaleTo(0.4f, 240, Easing.OutQuint); + logo.ScaleTo(logo_scale, 240, Easing.OutQuint); logo.Action = () => { @@ -122,14 +133,9 @@ namespace osu.Game.Screens.SelectV2 logo.FadeOut(120, Easing.Out); } - private partial class SoloModSelectOverlay : ModSelectOverlay + private partial class SoloModSelectOverlay : UserModSelectOverlay { protected override bool ShowPresets => true; - - public SoloModSelectOverlay() - : base(OverlayColourScheme.Aquamarine) - { - } } private partial class PlayerLoaderV2 : PlayerLoader diff --git a/osu.Game/Tests/Visual/Gameplay/ScoringTestScene.cs b/osu.Game/Tests/Visual/Gameplay/ScoringTestScene.cs index e7053e4202..6908f7f1b4 100644 --- a/osu.Game/Tests/Visual/Gameplay/ScoringTestScene.cs +++ b/osu.Game/Tests/Visual/Gameplay/ScoringTestScene.cs @@ -658,7 +658,6 @@ namespace osu.Game.Tests.Visual.Gameplay private partial class TestModSelectOverlay : UserModSelectOverlay { - protected override bool ShowModEffects => true; protected override bool ShowPresets => false; public TestModSelectOverlay() diff --git a/osu.Game/Tests/Visual/ScreenTestScene.cs b/osu.Game/Tests/Visual/ScreenTestScene.cs index 3cca1e59cc..f780b1a8f8 100644 --- a/osu.Game/Tests/Visual/ScreenTestScene.cs +++ b/osu.Game/Tests/Visual/ScreenTestScene.cs @@ -12,6 +12,7 @@ using osu.Framework.Testing; using osu.Game.Graphics; using osu.Game.Overlays; using osu.Game.Screens; +using osu.Game.Screens.Footer; namespace osu.Game.Tests.Visual { @@ -30,6 +31,9 @@ namespace osu.Game.Tests.Visual [Cached(typeof(IDialogOverlay))] protected DialogOverlay DialogOverlay { get; private set; } + [Cached] + private ScreenFooter footer; + protected ScreenTestScene() { base.Content.AddRange(new Drawable[] @@ -44,7 +48,8 @@ namespace osu.Game.Tests.Visual { RelativeSizeAxes = Axes.Both, Child = DialogOverlay = new DialogOverlay() - } + }, + footer = new ScreenFooter(), }); Stack.ScreenPushed += (_, newScreen) => Logger.Log($"{nameof(ScreenTestScene)} screen changed → {newScreen}");