1
0
mirror of https://github.com/ppy/osu.git synced 2026-05-17 18:13:18 +08:00
Files
osu-lazer/osu.Game.Tests/Visual/UserInterface/TestSceneScreenFooter.cs
T
Bartłomiej Dach 97ddc8e160 Fix footer buttons still being able to fire their action when temporarily hidden by overlay content (#37231)
In human words: I read [this forum
thread](https://osu.ppy.sh/community/forums/topics/2195138?n=1) today
and was horrified to see the user there opening the F3 options menu in
song select *when the F1 mod overlay was pulled up*, which is (a) not
intended UX, (b) looks terrible, and (c) just wrecks the game
behaviourally wholesale from start to end.

So with this change you don't get to open options via F3 while inside
mod overlay at all.

The `Action` shadowing is pretty ugly but I don't have better ideas.
Initially I tried to mess with `Enabled` (as I did once previously, see
https://github.com/ppy/osu/commit/36628e24f92c286b87c118c7c1bb9bc582895571),
but it's much more complicated in this case because the enabled state
needs to be restored when the buttons reappear, or it could change
independently while the buttons are temporarily hidden, etc. So I'd
rather just not deal with all that and invent a parallel scheme.
2026-04-08 00:40:50 +09:00

406 lines
18 KiB
C#

// 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 System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Screens;
using osu.Framework.Testing;
using osu.Game.Graphics;
using osu.Game.Graphics.UserInterface;
using osu.Game.Input.Bindings;
using osu.Game.Overlays;
using osu.Game.Overlays.Mods;
using osu.Game.Screens;
using osu.Game.Screens.Footer;
using osu.Game.Screens.Select;
using osuTK.Input;
namespace osu.Game.Tests.Visual.UserInterface
{
public partial class TestSceneScreenFooter : ScreenTestScene
{
[Test]
public void TestButtonsIn()
{
AddStep("push empty screen", () => LoadScreen(new TestScreen()));
AddStep("push screen", () => LoadScreen(new TestScreen
{
CreateButtons = () => new[]
{
new ScreenFooterButton { Text = "Button 1", Action = () => { } },
new ScreenFooterButton { Text = "Button 2", Action = () => { } },
new ScreenFooterButton { Text = "Button 3", Action = () => { } },
},
}));
}
[Test]
public void TestButtonsOut()
{
AddStep("push empty screen", () => LoadScreen(new TestScreen()));
AddStep("push screen", () => LoadScreen(new TestScreen
{
CreateButtons = () => new[]
{
new ScreenFooterButton { Text = "Button 1", Action = () => { } },
new ScreenFooterButton { Text = "Button 2", Action = () => { } },
new ScreenFooterButton { Text = "Button 3", Action = () => { } },
},
}));
AddStep("exit screen", () => Stack.Exit());
}
[Test]
public void TestReplaceButtons()
{
AddStep("push first screen", () => LoadScreen(new TestScreen
{
CreateButtons = () => new[]
{
new ScreenFooterButton { Text = "Button 1", Action = () => { } },
new ScreenFooterButton { Text = "Button 2", Action = () => { } },
new ScreenFooterButton { Text = "Button 3", Action = () => { } },
},
}));
AddStep("push second screen", () => LoadScreen(new TestScreen
{
CreateButtons = () => new[]
{
new ScreenFooterButton { Text = "Button 4", Action = () => { } },
new ScreenFooterButton { Text = "Button 5", Action = () => { } },
new ScreenFooterButton { Text = "Button 6", Action = () => { } },
},
}));
}
[Test]
public void TestFooterVisibility()
{
TestScreen screen = null!;
TestScreen screenWithoutFooter = null!;
AddAssert("footer hidden", () => ScreenFooter.State.Value, () => Is.EqualTo(Visibility.Hidden));
AddStep("push screen", () => LoadScreen(screen = new TestScreen
{
CreateButtons = () => new[]
{
new ScreenFooterButton { Text = "Button 1", Action = () => { } },
new ScreenFooterButton { Text = "Button 2", Action = () => { } },
new ScreenFooterButton { Text = "Button 3", Action = () => { } },
},
}));
AddUntilStep("wait until screen is loaded", () => screen.IsCurrentScreen(), () => Is.True);
AddAssert("footer shown", () => ScreenFooter.State.Value, () => Is.EqualTo(Visibility.Visible));
AddStep("push screen with no footer", () => LoadScreen(screenWithoutFooter = new TestScreen(showFooter: false)));
AddUntilStep("wait until screen is loaded", () => screenWithoutFooter.IsCurrentScreen(), () => Is.True);
AddAssert("footer hidden", () => ScreenFooter.State.Value, () => Is.EqualTo(Visibility.Hidden));
AddStep("exit screen", () => Stack.Exit());
AddUntilStep("wait until screen is loaded", () => screen.IsCurrentScreen(), () => Is.True);
AddAssert("footer shown", () => ScreenFooter.State.Value, () => Is.EqualTo(Visibility.Visible));
}
[Test]
public void TestExternalOverlayContent()
{
TestScreen screen = null!;
AddStep("push screen", () =>
{
ShearedOverlayContainer overlay = new TestShearedOverlayContainer();
LoadScreen(screen = new TestScreen
{
Overlay = overlay,
CreateButtons = () => new[]
{
new ScreenFooterButton(overlay)
{
AccentColour = Dependencies.Get<OsuColour>().Orange1,
Icon = FontAwesome.Solid.Toolbox,
Text = "One",
},
new ScreenFooterButton { Text = "Two", Action = () => { } },
new ScreenFooterButton { Text = "Three", Action = () => { } },
},
});
});
AddUntilStep("wait until screen is loaded", () => screen.IsCurrentScreen(), () => Is.True);
AddStep("show overlay", () => screen.Overlay.Show());
contentDisplayed();
AddAssert("other buttons hidden", () => ScreenFooter.ChildrenOfType<ScreenFooterButton>().Skip(1).All(b => b.Child.Parent!.Y > 0));
AddStep("hide overlay", () => screen.Overlay.Hide());
contentHidden();
AddAssert("other buttons returned", () => ScreenFooter.ChildrenOfType<ScreenFooterButton>().Skip(1).All(b => b.ChildrenOfType<Container>().First().Y == 0));
}
[Test]
public void TestButtonsHiddenByExternalOverlayContentCannotBeTriggered()
{
TestScreen screen = null!;
bool buttonTriggered = false;
AddStep("push screen", () =>
{
ShearedOverlayContainer overlay = new TestShearedOverlayContainer();
LoadScreen(screen = new TestScreen
{
Overlay = overlay,
CreateButtons = () => new[]
{
new ScreenFooterButton(overlay)
{
AccentColour = Dependencies.Get<OsuColour>().Orange1,
Icon = FontAwesome.Solid.Toolbox,
Text = "One",
},
new ScreenFooterButton { Text = "Two", Hotkey = GlobalAction.Select, Action = () => buttonTriggered = true },
new ScreenFooterButton { Text = "Three", Action = () => { } },
},
});
});
AddUntilStep("wait until screen is loaded", () => screen.IsCurrentScreen(), () => Is.True);
AddStep("show overlay", () => screen.Overlay.Show());
contentDisplayed();
AddStep("try direct click", () => ScreenFooter.ChildrenOfType<ScreenFooterButton>().ElementAt(1).TriggerClick());
AddAssert("action not triggered", () => buttonTriggered, () => Is.False);
AddStep("try hotkey", () => InputManager.Key(Key.Enter));
AddAssert("action not triggered", () => buttonTriggered, () => Is.False);
}
[Test]
public void TestTemporarilyShowFooter()
{
TestScreen screen = null!;
AddStep("push screen", () => LoadScreen(screen = new TestScreen(showFooter: false)));
AddUntilStep("wait until screen is loaded", () => screen.IsCurrentScreen(), () => Is.True);
AddAssert("footer hidden", () => ScreenFooter.State.Value, () => Is.EqualTo(Visibility.Hidden));
AddStep("show overlay", () => screen.Overlay.Show());
AddAssert("footer shown", () => ScreenFooter.State.Value, () => Is.EqualTo(Visibility.Visible));
contentDisplayed();
AddStep("hide overlay", () => screen.Overlay.Hide());
AddAssert("footer hidden", () => ScreenFooter.State.Value, () => Is.EqualTo(Visibility.Hidden));
contentHidden();
}
[Test]
public void TestShowOverlayHidesOtherOverlays()
{
TestScreen screen = null!;
AddStep("push screen", () =>
{
ShearedOverlayContainer overlay = new TestShearedOverlayContainer();
ModSelectOverlay secondOverlay = new ModSelectOverlay();
LoadScreen(screen = new TestScreen
{
Overlay = overlay,
SecondOverlay = secondOverlay,
CreateButtons = () => new[]
{
new ScreenFooterButton(overlay)
{
AccentColour = Dependencies.Get<OsuColour>().Orange1,
Icon = FontAwesome.Solid.Toolbox,
Text = "One",
},
new FooterButtonMods(secondOverlay),
new ScreenFooterButton { Text = "Two", Action = () => { } },
new ScreenFooterButton { Text = "Three", Action = () => { } },
},
});
});
AddUntilStep("wait until screen is loaded", () => screen.IsCurrentScreen(), () => Is.True);
AddStep("show mods overlay", () => ScreenFooter.ChildrenOfType<FooterButtonMods>().First().TriggerClick());
AddUntilStep("wait until overlay is shown", () => screen.SecondOverlay.State.Value, () => Is.EqualTo(Visibility.Visible));
AddAssert("first button still visible", () => ScreenFooter.ChildrenOfType<ScreenFooterButton>().First(b => b.Text == "One").Y, () => Is.EqualTo(0));
AddStep("show test overlay", () => ScreenFooter.ChildrenOfType<ScreenFooterButton>().First(b => b.Text == "One").TriggerClick());
AddUntilStep("wait until overlay is shown", () => screen.Overlay.State.Value, () => Is.EqualTo(Visibility.Visible));
AddAssert("mod overlay is hidden", () => screen.SecondOverlay.State.Value, () => Is.EqualTo(Visibility.Hidden));
AddStep("hide test overlay", () => screen.Overlay.Hide());
contentHidden();
AddAssert("other buttons returned", () => ScreenFooter.ChildrenOfType<ScreenFooterButton>().Skip(1).All(b => b.ChildrenOfType<Container>().First().Y == 0));
}
[Test]
public void TestButtonResizedAfterFooterIsDisplayed()
{
TestScreen screen = null!;
const float initial_width = 116;
const float width_increase = 124;
float secondButtonX = 0;
float overlayContentX = 0;
AddStep("push screen", () =>
{
ShearedOverlayContainer overlay = new TestShearedOverlayContainer();
LoadScreen(screen = new TestScreen
{
Overlay = overlay,
CreateButtons = () => new[]
{
new ScreenFooterButton(overlay)
{
AccentColour = Dependencies.Get<OsuColour>().Orange1,
Icon = FontAwesome.Solid.Toolbox,
Text = "One",
},
new ScreenFooterButton { Text = "Two", Action = () => { } },
new ScreenFooterButton { Text = "Three", Action = () => { } },
},
});
});
AddUntilStep("wait until screen is loaded", () => screen.IsCurrentScreen(), () => Is.True);
AddStep("save second button position", () => secondButtonX = ScreenFooter.ChildrenOfType<ScreenFooterButton>().ElementAt(1).X);
AddStep("resize active button", () => ScreenFooter.ChildrenOfType<ScreenFooterButton>().First().ResizeWidthTo(initial_width + width_increase, 300, Easing.OutQuint));
AddUntilStep("second button moved", () => ScreenFooter.ChildrenOfType<ScreenFooterButton>().ElementAt(1).X, () => Is.EqualTo(secondButtonX + width_increase).Within(0.001));
AddStep("resize active button back", () => this.ChildrenOfType<ScreenFooterButton>().First().ResizeWidthTo(initial_width, 300, Easing.OutQuint));
AddUntilStep("second button moved back", () => ScreenFooter.ChildrenOfType<ScreenFooterButton>().ElementAt(1).X, () => Is.EqualTo(secondButtonX).Within(0.001));
AddStep("show overlay", () => screen.Overlay.Show());
contentDisplayed();
AddAssert("other buttons hidden", () => ScreenFooter.ChildrenOfType<ScreenFooterButton>().Skip(1).All(b => b.Child.Parent!.Y > 0));
AddStep("save overlay content position", () => overlayContentX = ScreenFooter.ChildrenOfType<TestShearedOverlayContainer.TestFooterContent>().First().Parent!.Parent!.X);
AddStep("resize active button", () => ScreenFooter.ChildrenOfType<ScreenFooterButton>().First().ResizeWidthTo(initial_width + width_increase, 300, Easing.OutQuint));
AddUntilStep("overlay content moved", () => ScreenFooter.ChildrenOfType<TestShearedOverlayContainer.TestFooterContent>().First().Parent!.Parent!.X, () => Is.EqualTo(overlayContentX + width_increase).Within(0.001));
AddStep("resize active button back", () => this.ChildrenOfType<ScreenFooterButton>().First().ResizeWidthTo(initial_width, 300, Easing.OutQuint));
AddUntilStep("overlay content moved back", () => ScreenFooter.ChildrenOfType<TestShearedOverlayContainer.TestFooterContent>().First().Parent!.Parent!.X, () => Is.EqualTo(overlayContentX).Within(0.001));
AddStep("hide overlay", () => screen.Overlay.Hide());
contentHidden();
AddUntilStep("other buttons returned", () => ScreenFooter.ChildrenOfType<ScreenFooterButton>().Skip(1).All(b => b.ChildrenOfType<Container>().First().Y == 0));
}
private void contentHidden()
{
AddUntilStep("content hidden from footer", () => ScreenFooter.ChildrenOfType<TestShearedOverlayContainer.TestFooterContent>().SingleOrDefault()?.IsPresent != true);
}
private void contentDisplayed()
{
AddUntilStep("content displayed in footer", () => ScreenFooter.ChildrenOfType<TestShearedOverlayContainer.TestFooterContent>().Single().IsPresent);
}
private partial class TestScreen : OsuScreen
{
public override bool ShowFooter { get; }
public Func<IReadOnlyList<ScreenFooterButton>> CreateButtons = Array.Empty<ScreenFooterButton>;
public ShearedOverlayContainer Overlay = new TestShearedOverlayContainer();
public ShearedOverlayContainer SecondOverlay = new TestShearedOverlayContainer();
private IDisposable? overlayRegistration;
private IDisposable? secondOverlayRegistration;
[Cached]
private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Blue);
[Resolved]
private IOverlayManager? overlayManager { get; set; }
public TestScreen(bool showFooter = true)
{
ShowFooter = showFooter;
}
[BackgroundDependencyLoader]
private void load()
{
LoadComponent(Overlay);
LoadComponent(SecondOverlay);
}
protected override void LoadComplete()
{
base.LoadComplete();
overlayRegistration = overlayManager?.RegisterBlockingOverlay(Overlay);
secondOverlayRegistration = overlayManager?.RegisterBlockingOverlay(SecondOverlay);
}
public override IReadOnlyList<ScreenFooterButton> CreateFooterButtons() => CreateButtons.Invoke();
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
overlayRegistration?.Dispose();
secondOverlayRegistration?.Dispose();
}
}
private partial class TestShearedOverlayContainer : ShearedOverlayContainer
{
public TestShearedOverlayContainer()
: base(OverlayColourScheme.Orange)
{
}
[BackgroundDependencyLoader]
private void load()
{
Header.Title = "Test overlay";
Header.Description = "An overlay that is made purely for testing purposes.";
}
public override VisibilityContainer CreateFooterContent() => new TestFooterContent();
public partial class TestFooterContent : VisibilityContainer
{
[BackgroundDependencyLoader]
private void load()
{
AutoSizeAxes = Axes.Both;
InternalChild = new FillFlowContainer
{
AutoSizeAxes = Axes.Both,
Children = new[]
{
new ShearedButton { Width = 200, Text = "Action #1", Action = () => { } },
new ShearedButton { Width = 140, Text = "Action #2", Action = () => { } },
}
};
}
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);
}
}
}
}
}