1
0
mirror of https://github.com/ppy/osu.git synced 2026-05-13 20:33:35 +08:00

Fix replay/spectator scroll text not toggling with Ctrl+H (#37027)

Intended to add toggle but forgot.

This also fixes https://github.com/ppy/osu/issues/37012 via a convoluted
refactor of a lot of stuff. The basic overview is:

- Moved all replay overlay concerns out of `HUDOverlay`. We can display
this above everything confidently (i think).
- Split out `ReplayOverlay` and `ReplaySettingsOverlay` so the base
class can handle the visibility, hotkeys and everything that should be
shared with *all* replay overlay components going forward. `Ctrl+H` is
supposed to hide any of these kinds of details, and I'm sure we'll add
more in the future.
- Reorganised some things in `Player` so the new structure would work.
Mainly the overlays which add a black layer during fade out.
This commit is contained in:
Dean Herbert
2026-04-01 18:29:25 +09:00
committed by GitHub
Unverified
parent c4a49f647f
commit 279effe23c
12 changed files with 176 additions and 118 deletions
@@ -211,7 +211,7 @@ namespace osu.Game.Tests.Visual.Gameplay
AddStep("move mouse to centre of screen", () => InputManager.MoveMouseTo(Player.ScreenSpaceDrawQuad.Centre));
AddUntilStep("wait for settings overlay hidden", () => settingsOverlay().Expanded.Value, () => Is.False);
PlayerSettingsOverlay settingsOverlay() => Player.ChildrenOfType<PlayerSettingsOverlay>().Single();
ReplaySettingsOverlay settingsOverlay() => Player.ChildrenOfType<ReplaySettingsOverlay>().Single();
}
private void loadPlayerWithBeatmap(IBeatmap? beatmap = null)
@@ -156,7 +156,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
// components wrapped in skinnable target containers load asynchronously, potentially taking more than one frame to load.
// therefore use until step rather than direct assert to account for that.
AddUntilStep("all interactive elements removed", () => this.ChildrenOfType<Player>().All(p =>
!p.ChildrenOfType<PlayerSettingsOverlay>().Any() &&
!p.ChildrenOfType<ReplaySettingsOverlay>().Any() &&
!p.ChildrenOfType<HoldForMenuButton>().Any() &&
p.ChildrenOfType<ArgonSongProgressBar>().SingleOrDefault()?.Interactive == false));
@@ -300,7 +300,7 @@ namespace osu.Game.Tests.Visual.Navigation
AddStep("move cursor to right of screen too far", () => InputManager.MoveMouseTo(InputManager.ScreenSpaceDrawQuad.TopRight + new Vector2(10240, 0)));
AddUntilStep("settings not visible", () => getPlayerSettingsOverlay().DrawWidth, () => Is.EqualTo(0));
PlayerSettingsOverlay getPlayerSettingsOverlay() => ((Player)Game.ScreenStack.CurrentScreen).ChildrenOfType<PlayerSettingsOverlay>().SingleOrDefault();
ReplaySettingsOverlay getPlayerSettingsOverlay() => ((Player)Game.ScreenStack.CurrentScreen).ChildrenOfType<ReplaySettingsOverlay>().SingleOrDefault();
}
[Test]
+1 -1
View File
@@ -46,7 +46,7 @@ namespace osu.Game.Rulesets.Mods
player.DimmableStoryboard.IgnoreUserSettings.Value = true;
player.BreakOverlay.Hide();
player.OverlayComponents.Hide();
(player as ReplayPlayer)?.ReplayOverlay.Hide();
}
public bool PerformFail() => false;
@@ -39,6 +39,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
: base(score, new PlayerConfiguration { AllowUserInteraction = false })
{
this.spectatorPlayerClock = spectatorPlayerClock;
ShowSettingsOverlay = false;
}
[BackgroundDependencyLoader]
@@ -54,7 +56,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
// also applied in `MultiplayerPlayer.load()`
ScoreProcessor.ApplyNewJudgementsWhenFailed = true;
HUDOverlay.PlayerSettingsOverlay.Expire();
HUDOverlay.HoldToQuit.Expire();
}
@@ -65,7 +65,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
private readonly Room room;
private PlayerSettingsOverlay playerSettingsOverlay = null!;
private ReplaySettingsOverlay replaySettingsOverlay = null!;
private Bindable<bool> configSettingsOverlay = null!;
/// <summary>
@@ -138,7 +138,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
{
ReadyToStart = performInitialSeek,
},
playerSettingsOverlay = new PlayerSettingsOverlay
replaySettingsOverlay = new ReplaySettingsOverlay
{
Alpha = 0,
}
@@ -189,9 +189,9 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
private void updateVisibility()
{
if (configSettingsOverlay.Value)
playerSettingsOverlay.Show();
replaySettingsOverlay.Show();
else
playerSettingsOverlay.Hide();
replaySettingsOverlay.Hide();
}
protected override void Update()
@@ -0,0 +1,86 @@
// 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 osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Input.Bindings;
using osu.Framework.Input.Events;
using osu.Game.Configuration;
using osu.Game.Input.Bindings;
namespace osu.Game.Screens.Play.HUD
{
public partial class ReplayOverlay : CompositeDrawable, IKeyBindingHandler<GlobalAction>
{
public ReplaySettingsOverlay Settings { get; private set; } = null!;
private const int fade_duration = 200;
private Bindable<bool> configSettingsOverlay = null!;
private Container messageContainer = null!;
private Container content = null!;
[BackgroundDependencyLoader]
private void load(OsuConfigManager config)
{
RelativeSizeAxes = Axes.Both;
configSettingsOverlay = config.GetBindable<bool>(OsuSetting.ReplaySettingsOverlay);
InternalChild = content = new Container
{
Alpha = 0,
RelativeSizeAxes = Axes.Both,
Children = new Drawable[]
{
messageContainer = new Container
{
RelativeSizeAxes = Axes.Both,
Depth = float.MaxValue,
},
Settings = new ReplaySettingsOverlay(),
}
};
}
protected override void LoadComplete()
{
base.LoadComplete();
configSettingsOverlay.BindValueChanged(_ => updateVisibility(), true);
}
private void updateVisibility()
{
if (configSettingsOverlay.Value)
content.FadeIn(fade_duration, Easing.OutQuint);
else
content.FadeOut(fade_duration, Easing.OutQuint);
}
public bool OnPressed(KeyBindingPressEvent<GlobalAction> e)
{
if (e.Repeat)
return false;
switch (e.Action)
{
case GlobalAction.ToggleReplaySettings:
configSettingsOverlay.Value = !configSettingsOverlay.Value;
return true;
}
return false;
}
public void OnReleased(KeyBindingReleaseEvent<GlobalAction> e)
{
}
public override void Show() => this.FadeIn(fade_duration, Easing.OutQuint);
public override void Hide() => this.FadeOut(fade_duration, Easing.OutQuint);
public void SetMessage(ScrollingMessage scrollingMessage) => messageContainer.Child = scrollingMessage;
}
}
@@ -19,7 +19,7 @@ using osuTK.Graphics;
namespace osu.Game.Screens.Play.HUD
{
public partial class PlayerSettingsOverlay : ExpandingContainer
public partial class ReplaySettingsOverlay : ExpandingContainer
{
private const float padding = 10;
@@ -27,11 +27,6 @@ namespace osu.Game.Screens.Play.HUD
private const float player_settings_width = 270;
private const int fade_duration = 200;
public override void Show() => this.FadeIn(fade_duration);
public override void Hide() => this.FadeOut(fade_duration);
// we'll handle this ourselves because we have slightly custom logic.
protected override bool ExpandOnHover => false;
@@ -52,7 +47,7 @@ namespace osu.Game.Screens.Play.HUD
// while collapsed down, so let's avoid that.
protected override bool ComputeIsMaskedAway(RectangleF maskingBounds) => false;
public PlayerSettingsOverlay()
public ReplaySettingsOverlay()
: base(0, EXPANDED_WIDTH)
{
Origin = Anchor.TopRight;
@@ -81,11 +76,14 @@ namespace osu.Game.Screens.Play.HUD
Action = () => Expanded.Toggle()
});
AddInternal(new Box
AddRangeInternal(new Drawable[]
{
Colour = ColourInfo.GradientHorizontal(Color4.Black.Opacity(0), Color4.Black.Opacity(0.8f)),
Depth = float.MaxValue,
RelativeSizeAxes = Axes.Both,
new Box
{
Colour = ColourInfo.GradientHorizontal(Color4.Black.Opacity(0), Color4.Black.Opacity(0.8f)),
Depth = float.MaxValue,
RelativeSizeAxes = Axes.Both,
},
});
}
+2 -33
View File
@@ -38,11 +38,6 @@ namespace osu.Game.Screens.Play
public const Easing FADE_EASING = Easing.OutQuint;
/// <summary>
/// The total height of all the bottom of screen scoring elements.
/// </summary>
public float BottomScoringElementsHeight { get; private set; }
protected override bool ShouldBeConsideredForInput(Drawable child)
{
// HUD uses AlwaysVisible on child components so they can be in an updated state for next display.
@@ -56,7 +51,6 @@ namespace osu.Game.Screens.Play
public readonly ModDisplay ModDisplay;
public readonly HoldForMenuButton HoldToQuit;
public readonly PlayerSettingsOverlay PlayerSettingsOverlay;
[Cached]
private readonly ClicksPerSecondController clicksPerSecondController;
@@ -82,7 +76,6 @@ namespace osu.Game.Screens.Play
private Bindable<HUDVisibilityMode> configVisibilityMode;
private Bindable<bool> configLeaderboardVisibility;
private Bindable<bool> configSettingsOverlay;
private readonly BindableBool replayLoaded = new BindableBool();
@@ -116,8 +109,6 @@ namespace osu.Game.Screens.Play
public HUDOverlay([CanBeNull] DrawableRuleset drawableRuleset, IReadOnlyList<Mod> mods, PlayerConfiguration configuration)
{
Container rightSettings;
this.drawableRuleset = drawableRuleset;
this.mods = mods;
this.configuration = configuration;
@@ -170,17 +161,6 @@ namespace osu.Game.Screens.Play
HoldToQuit = CreateHoldForMenuButton(),
}
},
rightSettings = new Container
{
RelativeSizeAxes = Axes.Both,
Children = new Drawable[]
{
PlayerSettingsOverlay = new PlayerSettingsOverlay
{
Alpha = 0,
}
}
},
TopLeftElements = new FillFlowContainer
{
AutoSizeAxes = Axes.Both,
@@ -190,7 +170,7 @@ namespace osu.Game.Screens.Play
},
};
hideTargets = new List<Drawable> { mainComponents, TopRightElements, rightSettings };
hideTargets = new List<Drawable> { mainComponents, TopRightElements };
if (rulesetComponents != null)
hideTargets.Add(rulesetComponents);
@@ -210,7 +190,6 @@ namespace osu.Game.Screens.Play
configVisibilityMode = config.GetBindable<HUDVisibilityMode>(OsuSetting.HUDVisibilityMode);
configLeaderboardVisibility = config.GetBindable<bool>(OsuSetting.GameplayLeaderboard);
configSettingsOverlay = config.GetBindable<bool>(OsuSetting.ReplaySettingsOverlay);
if (configVisibilityMode.Value == HUDVisibilityMode.Never && !hasShownNotificationOnce)
{
@@ -238,7 +217,6 @@ namespace osu.Game.Screens.Play
holdingForHUD.BindValueChanged(_ => updateVisibility());
IsPlaying.BindValueChanged(_ => updateVisibility());
configVisibilityMode.BindValueChanged(_ => updateVisibility());
configSettingsOverlay.BindValueChanged(_ => updateVisibility());
replayLoaded.BindValueChanged(e =>
{
@@ -295,7 +273,7 @@ namespace osu.Game.Screens.Play
TopLeftElements.Y = 0;
if (highestBottomScreenSpace.HasValue && DrawHeight - BottomRightElements.DrawHeight > 0)
BottomRightElements.Y = BottomScoringElementsHeight = -Math.Clamp(DrawHeight - ToLocalSpace(highestBottomScreenSpace.Value).Y, 0, DrawHeight - BottomRightElements.DrawHeight);
BottomRightElements.Y = -Math.Clamp(DrawHeight - ToLocalSpace(highestBottomScreenSpace.Value).Y, 0, DrawHeight - BottomRightElements.DrawHeight);
else
BottomRightElements.Y = 0;
@@ -349,11 +327,6 @@ namespace osu.Game.Screens.Play
private void updateVisibility()
{
if (configSettingsOverlay.Value && replayLoaded.Value)
PlayerSettingsOverlay.Show();
else
PlayerSettingsOverlay.Hide();
if (ShowHud.Disabled)
return;
@@ -415,10 +388,6 @@ namespace osu.Game.Screens.Play
switch (e.Action)
{
case GlobalAction.ToggleReplaySettings:
configSettingsOverlay.Value = !configSettingsOverlay.Value;
return true;
case GlobalAction.HoldForHUD:
holdingForHUD.Value = true;
return false;
+19 -27
View File
@@ -320,32 +320,32 @@ namespace osu.Game.Screens.Play
OnRetry = Configuration.AllowUserInteraction ? () => Restart() : null,
OnQuit = () => PerformExitWithConfirmation(),
},
exitOverlay = new HotkeyExitOverlay
{
Action = () =>
{
if (!this.IsCurrentScreen()) return;
PerformExit(skipTransition: true);
},
},
});
if (cancellationToken.IsCancellationRequested)
return;
GameplayClockContainer.Add(exitOverlay = new HotkeyExitOverlay
{
Depth = float.MinValue,
Action = () =>
{
if (!this.IsCurrentScreen()) return;
PerformExit(skipTransition: true);
},
});
if (Configuration.AllowRestart)
{
rulesetSkinProvider.AddRange(new Drawable[]
GameplayClockContainer.Add(retryOverlay = new HotkeyRetryOverlay
{
retryOverlay = new HotkeyRetryOverlay
Depth = float.MinValue,
Action = () =>
{
Action = () =>
{
if (!this.IsCurrentScreen()) return;
if (!this.IsCurrentScreen()) return;
Restart(true);
},
Restart(true);
},
});
}
@@ -361,6 +361,9 @@ namespace osu.Game.Screens.Play
// we may want to limit this in the future to disallow rulesets from outright replacing elements the user expects to be there.
failAnimationContainer.Add(createOverlayComponents());
// Used by ReplaySettingsOverlay for button positioning.
dependencies.CacheAs(HUDOverlay);
if (!DrawableRuleset.AllowGameplayOverlays)
{
HUDOverlay.ShowHud.Value = false;
@@ -426,16 +429,6 @@ namespace osu.Game.Screens.Play
IsBreakTime.BindValueChanged(onBreakTimeChanged, true);
}
/// <summary>
/// Components which were created via <see cref="CreateOverlayComponents"/>.
/// </summary>
public Drawable OverlayComponents { get; private set; }
/// <summary>
/// Implement to add any components which should exist above gameplay but below the HUD.
/// </summary>
protected virtual Drawable CreateOverlayComponents() => Empty();
protected virtual GameplayClockContainer CreateGameplayClockContainer(WorkingBeatmap beatmap, double gameplayStart) => new MasterGameplayClockContainer(beatmap, gameplayStart);
private Drawable createUnderlayComponents(WorkingBeatmap working)
@@ -484,7 +477,6 @@ namespace osu.Game.Screens.Play
Children = new[]
{
DimmableStoryboard.OverlayLayerContainer.CreateProxy(),
OverlayComponents = CreateOverlayComponents(),
HUDOverlay = new HUDOverlay(DrawableRuleset, GameplayState.Mods, Configuration)
{
HoldToQuit =
+25 -23
View File
@@ -17,6 +17,7 @@ using osu.Game.Graphics.Containers;
using osu.Game.Input.Bindings;
using osu.Game.Rulesets.Mods;
using osu.Game.Scoring;
using osu.Game.Screens.Play.HUD;
using osu.Game.Screens.Play.Leaderboards;
using osu.Game.Screens.Play.PlayerSettings;
using osu.Game.Screens.Ranking;
@@ -47,6 +48,8 @@ namespace osu.Game.Screens.Play
private ReplayFailIndicator? failIndicator;
private PlaybackSettings? playbackSettings;
public ReplayOverlay ReplayOverlay { get; private set; } = null!;
protected override bool CheckModsAllowFailure()
{
// autoplay should be able to fail if the beatmap is not humanly beatable
@@ -80,7 +83,7 @@ namespace osu.Game.Screens.Play
/// Add a settings group to the HUD overlay. Intended to be used by rulesets to add replay-specific settings.
/// </summary>
/// <param name="settings">The settings group to be shown.</param>
public void AddSettings(PlayerSettingsGroup settings) => Schedule(() => HUDOverlay.PlayerSettingsOverlay.Add(settings));
public void AddSettings(PlayerSettingsGroup settings) => Schedule(() => ReplayOverlay.Settings.Add(settings));
[BackgroundDependencyLoader]
private void load(OsuConfigManager config)
@@ -90,6 +93,8 @@ namespace osu.Game.Screens.Play
AddInternal(leaderboardProvider);
GameplayClockContainer.Add(ReplayOverlay = new ReplayOverlay());
playbackSettings = new PlaybackSettings
{
Depth = float.MaxValue,
@@ -99,7 +104,25 @@ namespace osu.Game.Screens.Play
if (GameplayClockContainer is MasterGameplayClockContainer master)
playbackSettings.UserPlaybackRate.BindTo(master.UserPlaybackRate);
HUDOverlay.PlayerSettingsOverlay.AddAtStart(playbackSettings);
ReplayOverlay.Settings.AddAtStart(playbackSettings);
OsuTextFlowContainer message = new OsuTextFlowContainer(cp => cp.Font = OsuFont.Style.Body) { AutoSizeAxes = Axes.Both };
message.AddText("Watching ");
message.AddText(Score.ScoreInfo.User.Username, s => s.Font = s.Font.With(weight: FontWeight.SemiBold));
message.AddText(" play ");
message.AddText(Beatmap.Value.BeatmapInfo.GetDisplayTitleRomanisable(), s => s.Font = s.Font.With(weight: FontWeight.SemiBold));
message.AddText(" on ");
message.AddArbitraryDrawable(new PlayedOnText(Score.ScoreInfo.Date, false)
{
Font = OsuFont.Style.Body.With(weight: FontWeight.SemiBold),
});
ReplayOverlay.SetMessage(new ScrollingMessage(message)
{
Y = 96,
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
});
AddInternal(new RulesetSkinProvidingContainer(GameplayState.Ruleset, GameplayState.Beatmap, Beatmap.Value.Skin)
{
@@ -117,27 +140,6 @@ namespace osu.Game.Screens.Play
});
}
protected override Drawable CreateOverlayComponents()
{
OsuTextFlowContainer message = new OsuTextFlowContainer(cp => cp.Font = OsuFont.Style.Body) { AutoSizeAxes = Axes.Both };
message.AddText("Watching ");
message.AddText(Score.ScoreInfo.User.Username, s => s.Font = s.Font.With(weight: FontWeight.SemiBold));
message.AddText(" play ");
message.AddText(Beatmap.Value.BeatmapInfo.GetDisplayTitleRomanisable(), s => s.Font = s.Font.With(weight: FontWeight.SemiBold));
message.AddText(" on ");
message.AddArbitraryDrawable(new PlayedOnText(Score.ScoreInfo.Date, false)
{
Font = OsuFont.Style.Body.With(weight: FontWeight.SemiBold),
});
return new ScrollingMessage(message)
{
Y = 100,
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
};
}
protected override void PrepareReplay()
{
DrawableRuleset?.SetReplayScore(Score);
+25 -15
View File
@@ -12,6 +12,7 @@ using osu.Game.Online.Spectator;
using osu.Game.Rulesets.Replays;
using osu.Game.Rulesets.Replays.Types;
using osu.Game.Scoring;
using osu.Game.Screens.Play.HUD;
using osu.Game.Screens.Ranking;
namespace osu.Game.Screens.Play
@@ -23,29 +24,38 @@ namespace osu.Game.Screens.Play
private readonly Score score;
public bool ShowSettingsOverlay { get; init; } = true;
protected SpectatorPlayer(Score score, PlayerConfiguration? configuration = null)
: base(configuration)
{
this.score = score;
}
protected override Drawable CreateOverlayComponents()
[BackgroundDependencyLoader]
private void load()
{
// TODO: This should be customised for `MultiplayerSpectatorPlayer` to be static and only show the player name.
// Or maybe we should completely redesign this to show the user avatar and other things if that happens.
OsuTextFlowContainer message = new OsuTextFlowContainer(cp => cp.Font = OsuFont.Style.Body) { AutoSizeAxes = Axes.Both };
message.AddText("Watching ");
message.AddText(Score.ScoreInfo.User.Username, s => s.Font = s.Font.With(weight: FontWeight.SemiBold));
message.AddText(" play ");
message.AddText(Beatmap.Value.BeatmapInfo.GetDisplayTitleRomanisable(), s => s.Font = s.Font.With(weight: FontWeight.SemiBold));
message.AddText(" live", s => s.Font = s.Font.With(weight: FontWeight.Bold));
return new ScrollingMessage(message)
if (ShowSettingsOverlay)
{
Y = 100,
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
};
var replayOverlay = new ReplayOverlay();
GameplayClockContainer.Add(replayOverlay);
// TODO: This should be customised for `MultiplayerSpectatorPlayer` to be static and only show the player name.
// Or maybe we should completely redesign this to show the user avatar and other things if that happens.
OsuTextFlowContainer message = new OsuTextFlowContainer(cp => cp.Font = OsuFont.Style.Body) { AutoSizeAxes = Axes.Both };
message.AddText("Watching ");
message.AddText(Score.ScoreInfo.User.Username, s => s.Font = s.Font.With(weight: FontWeight.SemiBold));
message.AddText(" play ");
message.AddText(Beatmap.Value.BeatmapInfo.GetDisplayTitleRomanisable(), s => s.Font = s.Font.With(weight: FontWeight.SemiBold));
message.AddText(" live", s => s.Font = s.Font.With(weight: FontWeight.Bold));
replayOverlay.SetMessage(new ScrollingMessage(message)
{
Y = 96,
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
});
}
}
protected override void LoadComplete()