1
0
mirror of https://github.com/ppy/osu.git synced 2026-05-19 22:20:48 +08:00

Isolate footer behaviour to ScreenStackFooter, support subscreens

This commit is contained in:
Dan Balasescu
2025-10-16 17:56:15 +09:00
Unverified
parent 81a529200d
commit db3cc583e6
2 changed files with 226 additions and 63 deletions
+6 -63
View File
@@ -189,19 +189,14 @@ namespace osu.Game
/// </summary>
public readonly IBindable<OverlayActivation> OverlayActivationMode = new Bindable<OverlayActivation>();
/// <summary>
/// Whether the back button is currently displayed.
/// </summary>
private readonly IBindable<bool> backButtonVisibility = new BindableBool();
IBindable<LocalUserPlayingState> ILocalUserPlayInfo.PlayingState => UserPlayingState;
protected readonly Bindable<LocalUserPlayingState> UserPlayingState = new Bindable<LocalUserPlayingState>();
protected OsuScreenStack ScreenStack;
protected BackButton BackButton;
protected ScreenFooter ScreenFooter;
protected BackButton BackButton => screenStackFooter.BackButton;
protected ScreenFooter ScreenFooter => screenStackFooter.Footer;
protected SettingsOverlay Settings;
@@ -233,6 +228,8 @@ namespace osu.Game
private RealmDetachedBeatmapStore detachedBeatmapStore;
private ScreenStackFooter screenStackFooter;
private readonly string[] args;
private readonly List<OsuFocusedOverlayContainer> focusedOverlays = new List<OsuFocusedOverlayContainer>();
@@ -1132,12 +1129,6 @@ namespace osu.Game
{
backReceptor = new ScreenFooter.BackReceptor(),
ScreenStack = new OsuScreenStack { RelativeSizeAxes = Axes.Both },
BackButton = new BackButton(backReceptor)
{
Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomLeft,
Action = handleBackButton,
},
logoContainer = new Container { RelativeSizeAxes = Axes.Both },
// TODO: what is this? why is this?
// TODO: this is being screen scaled even though it's probably AN OVERLAY.
@@ -1150,7 +1141,7 @@ namespace osu.Game
{
Depth = -1,
RelativeSizeAxes = Axes.Both,
Child = ScreenFooter = new ScreenFooter(backReceptor)
Child = screenStackFooter = new ScreenStackFooter(ScreenStack, backReceptor)
{
// TODO: this is really really weird and should not exist.
RequestLogoInFront = inFront => ScreenContainer.ChangeChildDepth(logoContainer, inFront ? float.MinValue : 0),
@@ -1324,14 +1315,6 @@ namespace osu.Game
if (mode.NewValue != OverlayActivation.All) CloseAllOverlays();
};
backButtonVisibility.ValueChanged += visible =>
{
if (visible.NewValue)
BackButton.Show();
else
BackButton.Hide();
};
// Importantly, this should be run after binding PostNotification to the import handlers so they can present the import after game startup.
handleStartupImport();
}
@@ -1723,13 +1706,12 @@ namespace osu.Game
if (current != null)
{
backButtonVisibility.UnbindFrom(current.BackButtonVisibility);
OverlayActivationMode.UnbindFrom(current.OverlayActivationMode);
configUserActivity.UnbindFrom(current.Activity);
}
// Bind to new screen.
if (newScreen != null)
if (newScreen is OsuScreen newOsuScreen)
{
OverlayActivationMode.BindTo(newScreen.OverlayActivationMode);
configUserActivity.BindTo(newScreen.Activity);
@@ -1742,45 +1724,6 @@ namespace osu.Game
else
Toolbar.Show();
var newOsuScreen = (OsuScreen)newScreen;
if (newScreen.ShowFooter)
{
// the legacy back button should never display while the new footer is in use, as it
// contains its own local back button.
((BindableBool)backButtonVisibility).Value = false;
BackButton.Hide();
ScreenFooter.Show();
if (newOsuScreen.IsLoaded)
updateFooterButtons();
else
{
// ensure the current buttons are immediately disabled on screen change (so they can't be pressed).
ScreenFooter.SetButtons(Array.Empty<ScreenFooterButton>());
newOsuScreen.OnLoadComplete += _ => updateFooterButtons();
}
void updateFooterButtons()
{
var buttons = newScreen.CreateFooterButtons();
newOsuScreen.LoadComponentsAgainstScreenDependencies(buttons);
ScreenFooter.SetButtons(buttons);
ScreenFooter.Show();
}
}
else
{
backButtonVisibility.BindTo(newScreen.BackButtonVisibility);
ScreenFooter.SetButtons(Array.Empty<ScreenFooterButton>());
ScreenFooter.Hide();
}
skinEditor.SetTarget(newOsuScreen);
}
}
@@ -0,0 +1,220 @@
// 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.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Screens;
using osu.Game.Graphics.UserInterface;
namespace osu.Game.Screens.Footer
{
public partial class ScreenStackFooter : CompositeDrawable
{
/// <summary>
/// Called when logo tracking begins, intended to bring the osu! logo to the frontmost visually.
/// </summary>
public Action<bool>? RequestLogoInFront { private get; init; }
/// <summary>
/// The back button was pressed.
/// </summary>
public Action? BackButtonPressed { private get; init; }
/// <summary>
/// The (legacy) back button.
/// </summary>
public readonly BackButton BackButton;
/// <summary>
/// The footer.
/// </summary>
public readonly ScreenFooter Footer;
/// <summary>
/// Whether the legacy back button is currently displayed.
/// </summary>
private readonly IBindable<bool> backButtonVisibility = new BindableBool();
private readonly ScreenStackTracker screenTracker;
public ScreenStackFooter(ScreenStack screenStack, ScreenFooter.BackReceptor? backReceptor = null)
{
RelativeSizeAxes = Axes.Both;
InternalChildren = new Drawable[]
{
BackButton = new BackButton(backReceptor)
{
Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomLeft,
Action = () => BackButtonPressed?.Invoke(),
},
Footer = new ScreenFooter(backReceptor)
{
RequestLogoInFront = v => RequestLogoInFront?.Invoke(v),
BackButtonPressed = () => BackButtonPressed?.Invoke()
}
};
screenTracker = new ScreenStackTracker(screenStack);
screenTracker.ScreenChanged += onScreenChanged;
backButtonVisibility.ValueChanged += onBackButtonVisibilityChanged;
}
private void onScreenChanged(IScreen lastScreen, IScreen newScreen)
{
unbindScreen(lastScreen);
bindScreen(newScreen);
}
private void onBackButtonVisibilityChanged(ValueChangedEvent<bool> visible)
{
if (visible.NewValue)
BackButton.Show();
else
BackButton.Hide();
}
private void unbindScreen(IScreen screen)
{
if (screen is not OsuScreen osuScreen)
return;
backButtonVisibility.UnbindFrom(osuScreen.BackButtonVisibility);
}
private void bindScreen(IScreen screen)
{
if (screen is not OsuScreen osuScreen)
{
((BindableBool)backButtonVisibility).Value = true;
Footer.SetButtons([]);
Footer.Hide();
return;
}
if (osuScreen.ShowFooter)
{
// the legacy back button should never display while the new footer is in use, as it
// contains its own local back button.
((BindableBool)backButtonVisibility).Value = false;
Footer.Show();
if (osuScreen.IsLoaded)
updateFooterButtons();
else
{
// ensure the current buttons are immediately disabled on screen change (so they can't be pressed).
Footer.SetButtons([]);
osuScreen.OnLoadComplete += _ => updateFooterButtons();
}
void updateFooterButtons()
{
var buttons = osuScreen.CreateFooterButtons();
osuScreen.LoadComponentsAgainstScreenDependencies(buttons);
Footer.SetButtons(buttons);
Footer.Show();
}
}
else
{
backButtonVisibility.BindTo(osuScreen.BackButtonVisibility);
Footer.SetButtons([]);
Footer.Hide();
}
}
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
screenTracker.Dispose();
}
/// <summary>
/// Recursively represents a single screen stack and any nested subscreen stack.
/// </summary>
private class ScreenStackTracker : IDisposable
{
/// <summary>
/// Invoked when the leading screen changes.
/// </summary>
/// <remarks>
/// This differs from <see cref="ScreenStack.ScreenPushed"/> and <see cref="ScreenStack.ScreenExited"/>
/// because <c>lastScreen</c> and <c>newScreen</c> may be subscreens of the current screen stack.
/// <br />
/// As such, no assumptions may be made as to the relation of screens to this entry's <see cref="ScreenStack"/>.
/// </remarks>
public event ScreenChangedDelegate? ScreenChanged;
/// <summary>
/// The screen stack tracked by this entry.
/// </summary>
private readonly ScreenStack stack;
/// <summary>
/// An entry corresponding to the subscreen stack of the current screen, if any.
/// </summary>
private ScreenStackTracker? subScreenTracker;
/// <summary>
/// The screen which should be bound to the screen footer - the most nested subscreen.
/// </summary>
private IScreen leadingScreen => subScreenTracker?.leadingScreen ?? stack.CurrentScreen;
public ScreenStackTracker(ScreenStack stack)
{
this.stack = stack;
stack.ScreenPushed += onParentScreenChanged;
stack.ScreenExited += onParentScreenChanged;
}
private void onParentScreenChanged(IScreen lastScreen, IScreen newScreen)
{
// The screen which we will be UNBINDING from the screen footer later on.
IScreen lastLeadingScreen = subScreenTracker?.leadingScreen ?? lastScreen;
// Subscreens are attached to a parent screen, so when the parent changes the subscreen must also.
subScreenTracker?.Dispose();
subScreenTracker = null;
// Check if we've switched to a screen that has a subscreen.
if (newScreen is IHasSubScreenStack newStack)
{
subScreenTracker = new ScreenStackTracker(newStack.SubScreenStack);
subScreenTracker.ScreenChanged += onSubScreenScreenChanged;
}
ScreenChanged?.Invoke(lastLeadingScreen, leadingScreen);
}
private void onSubScreenScreenChanged(IScreen lastScreen, IScreen newScreen)
{
ScreenChanged?.Invoke(lastScreen, newScreen);
}
public void Dispose()
{
stack.ScreenPushed -= onParentScreenChanged;
stack.ScreenExited -= onParentScreenChanged;
if (subScreenTracker != null)
{
subScreenTracker.ScreenChanged -= onSubScreenScreenChanged;
subScreenTracker.Dispose();
}
}
}
}
}