From a8cbd400d36b8997a0ba87ae1ec55227381764fd Mon Sep 17 00:00:00 2001
From: Dean Herbert <pe@ppy.sh>
Date: Mon, 5 Oct 2020 13:17:13 +0900
Subject: [PATCH 01/21] Ensure virtual track time is long enough for test
 beatmaps

---
 osu.Game/Tests/Beatmaps/HitObjectSampleTest.cs |  5 ++---
 osu.Game/Tests/Visual/OsuTestScene.cs          | 10 ++++++----
 2 files changed, 8 insertions(+), 7 deletions(-)

diff --git a/osu.Game/Tests/Beatmaps/HitObjectSampleTest.cs b/osu.Game/Tests/Beatmaps/HitObjectSampleTest.cs
index ab4fb38657..1e43e5d148 100644
--- a/osu.Game/Tests/Beatmaps/HitObjectSampleTest.cs
+++ b/osu.Game/Tests/Beatmaps/HitObjectSampleTest.cs
@@ -180,9 +180,8 @@ namespace osu.Game.Tests.Beatmaps
             private readonly BeatmapInfo skinBeatmapInfo;
             private readonly IResourceStore<byte[]> resourceStore;
 
-            public TestWorkingBeatmap(BeatmapInfo skinBeatmapInfo, IResourceStore<byte[]> resourceStore, IBeatmap beatmap, Storyboard storyboard, IFrameBasedClock referenceClock, AudioManager audio,
-                                      double length = 60000)
-                : base(beatmap, storyboard, referenceClock, audio, length)
+            public TestWorkingBeatmap(BeatmapInfo skinBeatmapInfo, IResourceStore<byte[]> resourceStore, IBeatmap beatmap, Storyboard storyboard, IFrameBasedClock referenceClock, AudioManager audio)
+                : base(beatmap, storyboard, referenceClock, audio)
             {
                 this.skinBeatmapInfo = skinBeatmapInfo;
                 this.resourceStore = resourceStore;
diff --git a/osu.Game/Tests/Visual/OsuTestScene.cs b/osu.Game/Tests/Visual/OsuTestScene.cs
index b59a1db403..6e2fd0a6d7 100644
--- a/osu.Game/Tests/Visual/OsuTestScene.cs
+++ b/osu.Game/Tests/Visual/OsuTestScene.cs
@@ -23,6 +23,7 @@ using osu.Game.Online.API;
 using osu.Game.Overlays;
 using osu.Game.Rulesets;
 using osu.Game.Rulesets.Mods;
+using osu.Game.Rulesets.Objects;
 using osu.Game.Rulesets.UI;
 using osu.Game.Screens;
 using osu.Game.Storyboards;
@@ -222,18 +223,19 @@ namespace osu.Game.Tests.Visual
             /// <param name="storyboard">The storyboard.</param>
             /// <param name="referenceClock">An optional clock which should be used instead of a stopwatch for virtual time progression.</param>
             /// <param name="audio">Audio manager. Required if a reference clock isn't provided.</param>
-            /// <param name="length">The length of the returned virtual track.</param>
-            public ClockBackedTestWorkingBeatmap(IBeatmap beatmap, Storyboard storyboard, IFrameBasedClock referenceClock, AudioManager audio, double length = 60000)
+            public ClockBackedTestWorkingBeatmap(IBeatmap beatmap, Storyboard storyboard, IFrameBasedClock referenceClock, AudioManager audio)
                 : base(beatmap, storyboard, audio)
             {
+                double lastObjectTime = beatmap.HitObjects.LastOrDefault()?.GetEndTime() ?? 60000;
+
                 if (referenceClock != null)
                 {
                     store = new TrackVirtualStore(referenceClock);
                     audio.AddItem(store);
-                    track = store.GetVirtual(length);
+                    track = store.GetVirtual(lastObjectTime);
                 }
                 else
-                    track = audio?.Tracks.GetVirtual(length);
+                    track = audio?.Tracks.GetVirtual(lastObjectTime);
             }
 
             ~ClockBackedTestWorkingBeatmap()

From 21bf93a7c2b6d07e4e825c6b14f59a4ea3edd0af Mon Sep 17 00:00:00 2001
From: Dean Herbert <pe@ppy.sh>
Date: Mon, 5 Oct 2020 13:29:36 +0900
Subject: [PATCH 02/21] Ensure there's a buffer after the last hitobject to
 allow certain replay tests to complete correctly

---
 osu.Game/Tests/Visual/OsuTestScene.cs | 8 +++++---
 1 file changed, 5 insertions(+), 3 deletions(-)

diff --git a/osu.Game/Tests/Visual/OsuTestScene.cs b/osu.Game/Tests/Visual/OsuTestScene.cs
index 6e2fd0a6d7..e3f07dbad4 100644
--- a/osu.Game/Tests/Visual/OsuTestScene.cs
+++ b/osu.Game/Tests/Visual/OsuTestScene.cs
@@ -226,16 +226,18 @@ namespace osu.Game.Tests.Visual
             public ClockBackedTestWorkingBeatmap(IBeatmap beatmap, Storyboard storyboard, IFrameBasedClock referenceClock, AudioManager audio)
                 : base(beatmap, storyboard, audio)
             {
-                double lastObjectTime = beatmap.HitObjects.LastOrDefault()?.GetEndTime() ?? 60000;
+                var lastHitObject = beatmap.HitObjects.LastOrDefault();
+
+                double trackLength = lastHitObject?.GetEndTime() + 2000 ?? 60000;
 
                 if (referenceClock != null)
                 {
                     store = new TrackVirtualStore(referenceClock);
                     audio.AddItem(store);
-                    track = store.GetVirtual(lastObjectTime);
+                    track = store.GetVirtual(trackLength);
                 }
                 else
-                    track = audio?.Tracks.GetVirtual(lastObjectTime);
+                    track = audio?.Tracks.GetVirtual(trackLength);
             }
 
             ~ClockBackedTestWorkingBeatmap()

From 4b8188065504cf88de4c8cb487a180f1a0696904 Mon Sep 17 00:00:00 2001
From: Dean Herbert <pe@ppy.sh>
Date: Mon, 5 Oct 2020 14:04:04 +0900
Subject: [PATCH 03/21] Account for potentially longer non-last objects

---
 osu.Game/Tests/Visual/OsuTestScene.cs | 6 ++++--
 1 file changed, 4 insertions(+), 2 deletions(-)

diff --git a/osu.Game/Tests/Visual/OsuTestScene.cs b/osu.Game/Tests/Visual/OsuTestScene.cs
index e3f07dbad4..8886188d95 100644
--- a/osu.Game/Tests/Visual/OsuTestScene.cs
+++ b/osu.Game/Tests/Visual/OsuTestScene.cs
@@ -226,9 +226,11 @@ namespace osu.Game.Tests.Visual
             public ClockBackedTestWorkingBeatmap(IBeatmap beatmap, Storyboard storyboard, IFrameBasedClock referenceClock, AudioManager audio)
                 : base(beatmap, storyboard, audio)
             {
-                var lastHitObject = beatmap.HitObjects.LastOrDefault();
+                double trackLength = 60000;
 
-                double trackLength = lastHitObject?.GetEndTime() + 2000 ?? 60000;
+                if (beatmap.HitObjects.Count > 0)
+                    // add buffer after last hitobject to allow for final replay frames etc.
+                    trackLength = beatmap.HitObjects.Max(h => h.GetEndTime()) + 2000;
 
                 if (referenceClock != null)
                 {

From 7fead6ee41dcf4a242eec2d0b13df47d6c2fd50c Mon Sep 17 00:00:00 2001
From: Dean Herbert <pe@ppy.sh>
Date: Mon, 5 Oct 2020 14:22:32 +0900
Subject: [PATCH 04/21] Add comment making mania test behaviour clearer

---
 osu.Game.Rulesets.Mania.Tests/TestSceneOutOfOrderHits.cs | 1 +
 1 file changed, 1 insertion(+)

diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneOutOfOrderHits.cs b/osu.Game.Rulesets.Mania.Tests/TestSceneOutOfOrderHits.cs
index ab840e1c46..e8c2472c3b 100644
--- a/osu.Game.Rulesets.Mania.Tests/TestSceneOutOfOrderHits.cs
+++ b/osu.Game.Rulesets.Mania.Tests/TestSceneOutOfOrderHits.cs
@@ -35,6 +35,7 @@ namespace osu.Game.Rulesets.Mania.Tests
 
                 objects.Add(new Note { StartTime = time });
 
+                // don't hit the first note
                 if (i > 0)
                 {
                     frames.Add(new ManiaReplayFrame(time + 10, ManiaAction.Key1));

From 71e373ff511de83b10282992570165f087741551 Mon Sep 17 00:00:00 2001
From: Dean Herbert <pe@ppy.sh>
Date: Thu, 29 Oct 2020 16:11:25 +0900
Subject: [PATCH 05/21] Make results panels aware of whether they are a local
 score that has just been set

---
 osu.Game.Tests/Visual/Ranking/TestSceneAccuracyCircle.cs  | 2 +-
 osu.Game.Tests/Visual/Ranking/TestSceneScorePanel.cs      | 2 +-
 .../Screens/Ranking/Expanded/Accuracy/AccuracyCircle.cs   | 2 +-
 .../Ranking/Expanded/ExpandedPanelMiddleContent.cs        | 8 ++++++--
 osu.Game/Screens/Ranking/ResultsScreen.cs                 | 2 +-
 osu.Game/Screens/Ranking/ScorePanel.cs                    | 7 +++++--
 osu.Game/Screens/Ranking/ScorePanelList.cs                | 5 +++--
 7 files changed, 18 insertions(+), 10 deletions(-)

diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneAccuracyCircle.cs b/osu.Game.Tests/Visual/Ranking/TestSceneAccuracyCircle.cs
index 1e87893f39..f69ccc1773 100644
--- a/osu.Game.Tests/Visual/Ranking/TestSceneAccuracyCircle.cs
+++ b/osu.Game.Tests/Visual/Ranking/TestSceneAccuracyCircle.cs
@@ -120,7 +120,7 @@ namespace osu.Game.Tests.Visual.Ranking
                         }
                     }
                 },
-                new AccuracyCircle(score)
+                new AccuracyCircle(score, false)
                 {
                     Anchor = Anchor.Centre,
                     Origin = Anchor.Centre,
diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneScorePanel.cs b/osu.Game.Tests/Visual/Ranking/TestSceneScorePanel.cs
index 250fdc5ebd..5af55e99f8 100644
--- a/osu.Game.Tests/Visual/Ranking/TestSceneScorePanel.cs
+++ b/osu.Game.Tests/Visual/Ranking/TestSceneScorePanel.cs
@@ -103,7 +103,7 @@ namespace osu.Game.Tests.Visual.Ranking
 
         private void addPanelStep(ScoreInfo score, PanelState state = PanelState.Expanded) => AddStep("add panel", () =>
         {
-            Child = panel = new ScorePanel(score)
+            Child = panel = new ScorePanel(score, true)
             {
                 Anchor = Anchor.Centre,
                 Origin = Anchor.Centre,
diff --git a/osu.Game/Screens/Ranking/Expanded/Accuracy/AccuracyCircle.cs b/osu.Game/Screens/Ranking/Expanded/Accuracy/AccuracyCircle.cs
index 45da23f1f9..337665b51f 100644
--- a/osu.Game/Screens/Ranking/Expanded/Accuracy/AccuracyCircle.cs
+++ b/osu.Game/Screens/Ranking/Expanded/Accuracy/AccuracyCircle.cs
@@ -78,7 +78,7 @@ namespace osu.Game.Screens.Ranking.Expanded.Accuracy
         private Container<RankBadge> badges;
         private RankText rankText;
 
-        public AccuracyCircle(ScoreInfo score)
+        public AccuracyCircle(ScoreInfo score, bool withFlair)
         {
             this.score = score;
         }
diff --git a/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs b/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs
index 30747438c3..5f8609d190 100644
--- a/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs
+++ b/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs
@@ -29,6 +29,8 @@ namespace osu.Game.Screens.Ranking.Expanded
         private const float padding = 10;
 
         private readonly ScoreInfo score;
+        private readonly bool withFlair;
+
         private readonly List<StatisticDisplay> statisticDisplays = new List<StatisticDisplay>();
 
         private FillFlowContainer starAndModDisplay;
@@ -41,9 +43,11 @@ namespace osu.Game.Screens.Ranking.Expanded
         /// Creates a new <see cref="ExpandedPanelMiddleContent"/>.
         /// </summary>
         /// <param name="score">The score to display.</param>
-        public ExpandedPanelMiddleContent(ScoreInfo score)
+        /// <param name="withFlair">Whether to add flair for a new score being set.</param>
+        public ExpandedPanelMiddleContent(ScoreInfo score, bool withFlair = false)
         {
             this.score = score;
+            this.withFlair = withFlair;
 
             RelativeSizeAxes = Axes.Both;
             Masking = true;
@@ -116,7 +120,7 @@ namespace osu.Game.Screens.Ranking.Expanded
                                     Margin = new MarginPadding { Top = 40 },
                                     RelativeSizeAxes = Axes.X,
                                     Height = 230,
-                                    Child = new AccuracyCircle(score)
+                                    Child = new AccuracyCircle(score, withFlair)
                                     {
                                         Anchor = Anchor.Centre,
                                         Origin = Anchor.Centre,
diff --git a/osu.Game/Screens/Ranking/ResultsScreen.cs b/osu.Game/Screens/Ranking/ResultsScreen.cs
index 026ce01857..f8bdf0140c 100644
--- a/osu.Game/Screens/Ranking/ResultsScreen.cs
+++ b/osu.Game/Screens/Ranking/ResultsScreen.cs
@@ -149,7 +149,7 @@ namespace osu.Game.Screens.Ranking
             };
 
             if (Score != null)
-                ScorePanelList.AddScore(Score);
+                ScorePanelList.AddScore(Score, true);
 
             if (player != null && allowRetry)
             {
diff --git a/osu.Game/Screens/Ranking/ScorePanel.cs b/osu.Game/Screens/Ranking/ScorePanel.cs
index ee97ee55eb..6e6227da38 100644
--- a/osu.Game/Screens/Ranking/ScorePanel.cs
+++ b/osu.Game/Screens/Ranking/ScorePanel.cs
@@ -85,6 +85,8 @@ namespace osu.Game.Screens.Ranking
 
         public readonly ScoreInfo Score;
 
+        private readonly bool isNewLocalScore;
+
         private Container content;
 
         private Container topLayerContainer;
@@ -97,9 +99,10 @@ namespace osu.Game.Screens.Ranking
         private Container middleLayerContentContainer;
         private Drawable middleLayerContent;
 
-        public ScorePanel(ScoreInfo score)
+        public ScorePanel(ScoreInfo score, bool isNewLocalScore = false)
         {
             Score = score;
+            this.isNewLocalScore = isNewLocalScore;
         }
 
         [BackgroundDependencyLoader]
@@ -209,7 +212,7 @@ namespace osu.Game.Screens.Ranking
                     middleLayerBackground.FadeColour(expanded_middle_layer_colour, resize_duration, Easing.OutQuint);
 
                     topLayerContentContainer.Add(topLayerContent = new ExpandedPanelTopContent(Score.User).With(d => d.Alpha = 0));
-                    middleLayerContentContainer.Add(middleLayerContent = new ExpandedPanelMiddleContent(Score).With(d => d.Alpha = 0));
+                    middleLayerContentContainer.Add(middleLayerContent = new ExpandedPanelMiddleContent(Score, isNewLocalScore).With(d => d.Alpha = 0));
                     break;
 
                 case PanelState.Contracted:
diff --git a/osu.Game/Screens/Ranking/ScorePanelList.cs b/osu.Game/Screens/Ranking/ScorePanelList.cs
index 0d7d339df0..cc163ba762 100644
--- a/osu.Game/Screens/Ranking/ScorePanelList.cs
+++ b/osu.Game/Screens/Ranking/ScorePanelList.cs
@@ -95,9 +95,10 @@ namespace osu.Game.Screens.Ranking
         /// Adds a <see cref="ScoreInfo"/> to this list.
         /// </summary>
         /// <param name="score">The <see cref="ScoreInfo"/> to add.</param>
-        public ScorePanel AddScore(ScoreInfo score)
+        /// <param name="isNewLocalScore">Whether this is a score that has just been achieved locally. Controls whether flair is added to the display or not.</param>
+        public ScorePanel AddScore(ScoreInfo score, bool isNewLocalScore = false)
         {
-            var panel = new ScorePanel(score)
+            var panel = new ScorePanel(score, isNewLocalScore)
             {
                 Anchor = Anchor.CentreLeft,
                 Origin = Anchor.CentreLeft,

From 11f85779d5222f24fa9d0edd8097398417407b6a Mon Sep 17 00:00:00 2001
From: Dean Herbert <pe@ppy.sh>
Date: Thu, 29 Oct 2020 17:03:45 +0900
Subject: [PATCH 06/21] Fix panel expanded state being updated multiple times
 unnecessarily

---
 osu.Game/Screens/Ranking/ScorePanelList.cs | 17 ++++++++++++-----
 1 file changed, 12 insertions(+), 5 deletions(-)

diff --git a/osu.Game/Screens/Ranking/ScorePanelList.cs b/osu.Game/Screens/Ranking/ScorePanelList.cs
index cc163ba762..e85580a734 100644
--- a/osu.Game/Screens/Ranking/ScorePanelList.cs
+++ b/osu.Game/Screens/Ranking/ScorePanelList.cs
@@ -119,7 +119,10 @@ namespace osu.Game.Screens.Ranking
             }));
 
             if (SelectedScore.Value == score)
-                selectedScoreChanged(new ValueChangedEvent<ScoreInfo>(SelectedScore.Value, SelectedScore.Value));
+            {
+                if (IsLoaded)
+                    SelectedScore.TriggerChange();
+            }
             else
             {
                 // We want the scroll position to remain relative to the expanded panel. When a new panel is added after the expanded panel, nothing needs to be done.
@@ -143,11 +146,15 @@ namespace osu.Game.Screens.Ranking
         /// <param name="score">The <see cref="ScoreInfo"/> to present.</param>
         private void selectedScoreChanged(ValueChangedEvent<ScoreInfo> score)
         {
-            // Contract the old panel.
-            foreach (var t in flow.Where(t => t.Panel.Score == score.OldValue))
+            // avoid contracting panels unnecessarily when TriggerChange is fired manually.
+            if (score.OldValue != score.NewValue)
             {
-                t.Panel.State = PanelState.Contracted;
-                t.Margin = new MarginPadding();
+                // Contract the old panel.
+                foreach (var t in flow.Where(t => t.Panel.Score == score.OldValue))
+                {
+                    t.Panel.State = PanelState.Contracted;
+                    t.Margin = new MarginPadding();
+                }
             }
 
             // Find the panel corresponding to the new score.

From 0a0239a7c799b88049f4a4ca524a58d6f6839a2d Mon Sep 17 00:00:00 2001
From: Dean Herbert <pe@ppy.sh>
Date: Thu, 29 Oct 2020 17:04:33 +0900
Subject: [PATCH 07/21] Only play results panel animation once (and only for
 the local user)

---
 .../Ranking/Expanded/ExpandedPanelMiddleContent.cs    |  3 +++
 osu.Game/Screens/Ranking/ScorePanel.cs                | 11 +++++++----
 2 files changed, 10 insertions(+), 4 deletions(-)

diff --git a/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs b/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs
index 5f8609d190..711763330c 100644
--- a/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs
+++ b/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs
@@ -270,6 +270,9 @@ namespace osu.Game.Screens.Ranking.Expanded
                         delay += 200;
                     }
                 }
+
+                if (!withFlair)
+                    FinishTransforms(true);
             });
         }
     }
diff --git a/osu.Game/Screens/Ranking/ScorePanel.cs b/osu.Game/Screens/Ranking/ScorePanel.cs
index 6e6227da38..df710e4eb8 100644
--- a/osu.Game/Screens/Ranking/ScorePanel.cs
+++ b/osu.Game/Screens/Ranking/ScorePanel.cs
@@ -85,7 +85,7 @@ namespace osu.Game.Screens.Ranking
 
         public readonly ScoreInfo Score;
 
-        private readonly bool isNewLocalScore;
+        private bool displayWithFlair;
 
         private Container content;
 
@@ -102,7 +102,7 @@ namespace osu.Game.Screens.Ranking
         public ScorePanel(ScoreInfo score, bool isNewLocalScore = false)
         {
             Score = score;
-            this.isNewLocalScore = isNewLocalScore;
+            displayWithFlair = isNewLocalScore;
         }
 
         [BackgroundDependencyLoader]
@@ -191,7 +191,7 @@ namespace osu.Game.Screens.Ranking
 
                 state = value;
 
-                if (LoadState >= LoadState.Ready)
+                if (IsLoaded)
                     updateState();
 
                 StateChanged?.Invoke(value);
@@ -212,7 +212,10 @@ namespace osu.Game.Screens.Ranking
                     middleLayerBackground.FadeColour(expanded_middle_layer_colour, resize_duration, Easing.OutQuint);
 
                     topLayerContentContainer.Add(topLayerContent = new ExpandedPanelTopContent(Score.User).With(d => d.Alpha = 0));
-                    middleLayerContentContainer.Add(middleLayerContent = new ExpandedPanelMiddleContent(Score, isNewLocalScore).With(d => d.Alpha = 0));
+                    middleLayerContentContainer.Add(middleLayerContent = new ExpandedPanelMiddleContent(Score, displayWithFlair).With(d => d.Alpha = 0));
+
+                    // only the first expanded display should happen with flair.
+                    displayWithFlair = false;
                     break;
 
                 case PanelState.Contracted:

From 4dec46b33e977e8351f916a2c60c71a53a20b08a Mon Sep 17 00:00:00 2001
From: Dean Herbert <pe@ppy.sh>
Date: Thu, 29 Oct 2020 17:52:58 +0900
Subject: [PATCH 08/21] Attempt to fix in a less destructive way for now

---
 osu.Game/Tests/Visual/OsuTestScene.cs | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/osu.Game/Tests/Visual/OsuTestScene.cs b/osu.Game/Tests/Visual/OsuTestScene.cs
index 8886188d95..e32ed07863 100644
--- a/osu.Game/Tests/Visual/OsuTestScene.cs
+++ b/osu.Game/Tests/Visual/OsuTestScene.cs
@@ -230,7 +230,7 @@ namespace osu.Game.Tests.Visual
 
                 if (beatmap.HitObjects.Count > 0)
                     // add buffer after last hitobject to allow for final replay frames etc.
-                    trackLength = beatmap.HitObjects.Max(h => h.GetEndTime()) + 2000;
+                    trackLength = Math.Max(trackLength, beatmap.HitObjects.Max(h => h.GetEndTime()) + 2000);
 
                 if (referenceClock != null)
                 {

From d69d78ab5d9278aaf31d20bd2895f664d1e3c2f1 Mon Sep 17 00:00:00 2001
From: Dean Herbert <pe@ppy.sh>
Date: Thu, 29 Oct 2020 15:20:10 +0900
Subject: [PATCH 09/21] Update namespace references

---
 osu.Game.Rulesets.Taiko/UI/DrumRollHitContainer.cs | 1 +
 osu.Game/Rulesets/UI/HitObjectContainer.cs         | 1 +
 2 files changed, 2 insertions(+)

diff --git a/osu.Game.Rulesets.Taiko/UI/DrumRollHitContainer.cs b/osu.Game.Rulesets.Taiko/UI/DrumRollHitContainer.cs
index fde42bec04..9bfb6aa839 100644
--- a/osu.Game.Rulesets.Taiko/UI/DrumRollHitContainer.cs
+++ b/osu.Game.Rulesets.Taiko/UI/DrumRollHitContainer.cs
@@ -2,6 +2,7 @@
 // See the LICENCE file in the repository root for full licence text.
 
 using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Performance;
 using osu.Game.Rulesets.Objects.Drawables;
 using osu.Game.Rulesets.Taiko.Objects.Drawables;
 using osu.Game.Rulesets.UI.Scrolling;
diff --git a/osu.Game/Rulesets/UI/HitObjectContainer.cs b/osu.Game/Rulesets/UI/HitObjectContainer.cs
index 9a0217a1eb..4cadfa9ad4 100644
--- a/osu.Game/Rulesets/UI/HitObjectContainer.cs
+++ b/osu.Game/Rulesets/UI/HitObjectContainer.cs
@@ -6,6 +6,7 @@ using System.Linq;
 using osu.Framework.Bindables;
 using osu.Framework.Graphics;
 using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Performance;
 using osu.Game.Rulesets.Objects.Drawables;
 
 namespace osu.Game.Rulesets.UI

From 3491dea9e2aece3ab76f0b931a5c9ef599e6eba4 Mon Sep 17 00:00:00 2001
From: Dean Herbert <pe@ppy.sh>
Date: Thu, 29 Oct 2020 18:51:54 +0900
Subject: [PATCH 10/21] Fix scroll logic running before children may be alive
 in flow

---
 osu.Game/Screens/Ranking/ScorePanelList.cs | 45 ++++++++++++----------
 1 file changed, 25 insertions(+), 20 deletions(-)

diff --git a/osu.Game/Screens/Ranking/ScorePanelList.cs b/osu.Game/Screens/Ranking/ScorePanelList.cs
index e85580a734..4325d317c4 100644
--- a/osu.Game/Screens/Ranking/ScorePanelList.cs
+++ b/osu.Game/Screens/Ranking/ScorePanelList.cs
@@ -118,22 +118,24 @@ namespace osu.Game.Screens.Ranking
                 d.Origin = Anchor.Centre;
             }));
 
-            if (SelectedScore.Value == score)
+            if (IsLoaded)
             {
-                if (IsLoaded)
-                    SelectedScore.TriggerChange();
-            }
-            else
-            {
-                // We want the scroll position to remain relative to the expanded panel. When a new panel is added after the expanded panel, nothing needs to be done.
-                // But when a panel is added before the expanded panel, we need to offset the scroll position by the width of the new panel.
-                if (expandedPanel != null && flow.GetPanelIndex(score) < flow.GetPanelIndex(expandedPanel.Score))
+                if (SelectedScore.Value == score)
                 {
-                    // A somewhat hacky property is used here because we need to:
-                    // 1) Scroll after the scroll container's visible range is updated.
-                    // 2) Scroll before the scroll container's scroll position is updated.
-                    // Without this, we would have a 1-frame positioning error which looks very jarring.
-                    scroll.InstantScrollTarget = (scroll.InstantScrollTarget ?? scroll.Target) + ScorePanel.CONTRACTED_WIDTH + panel_spacing;
+                    SelectedScore.TriggerChange();
+                }
+                else
+                {
+                    // We want the scroll position to remain relative to the expanded panel. When a new panel is added after the expanded panel, nothing needs to be done.
+                    // But when a panel is added before the expanded panel, we need to offset the scroll position by the width of the new panel.
+                    if (expandedPanel != null && flow.GetPanelIndex(score) < flow.GetPanelIndex(expandedPanel.Score))
+                    {
+                        // A somewhat hacky property is used here because we need to:
+                        // 1) Scroll after the scroll container's visible range is updated.
+                        // 2) Scroll before the scroll container's scroll position is updated.
+                        // Without this, we would have a 1-frame positioning error which looks very jarring.
+                        scroll.InstantScrollTarget = (scroll.InstantScrollTarget ?? scroll.Target) + ScorePanel.CONTRACTED_WIDTH + panel_spacing;
+                    }
                 }
             }
 
@@ -170,12 +172,15 @@ namespace osu.Game.Screens.Ranking
             expandedTrackingComponent.Margin = new MarginPadding { Horizontal = expanded_panel_spacing };
             expandedPanel.State = PanelState.Expanded;
 
-            // Scroll to the new panel. This is done manually since we need:
-            // 1) To scroll after the scroll container's visible range is updated.
-            // 2) To account for the centre anchor/origins of panels.
-            // In the end, it's easier to compute the scroll position manually.
-            float scrollOffset = flow.GetPanelIndex(expandedPanel.Score) * (ScorePanel.CONTRACTED_WIDTH + panel_spacing);
-            scroll.ScrollTo(scrollOffset);
+            SchedulerAfterChildren.Add(() =>
+            {
+                // Scroll to the new panel. This is done manually since we need:
+                // 1) To scroll after the scroll container's visible range is updated.
+                // 2) To account for the centre anchor/origins of panels.
+                // In the end, it's easier to compute the scroll position manually.
+                float scrollOffset = flow.GetPanelIndex(expandedPanel.Score) * (ScorePanel.CONTRACTED_WIDTH + panel_spacing);
+                scroll.ScrollTo(scrollOffset);
+            });
         }
 
         protected override void Update()

From 7be4dfabd8d384107b5a21a78ae6fa97f09fbeaa Mon Sep 17 00:00:00 2001
From: Dean Herbert <pe@ppy.sh>
Date: Thu, 29 Oct 2020 20:23:15 +0900
Subject: [PATCH 11/21] Revert "Update namespace references"

This reverts commit d69d78ab5d9278aaf31d20bd2895f664d1e3c2f1.
---
 osu.Game.Rulesets.Taiko/UI/DrumRollHitContainer.cs | 1 -
 osu.Game/Rulesets/UI/HitObjectContainer.cs         | 1 -
 2 files changed, 2 deletions(-)

diff --git a/osu.Game.Rulesets.Taiko/UI/DrumRollHitContainer.cs b/osu.Game.Rulesets.Taiko/UI/DrumRollHitContainer.cs
index 9bfb6aa839..fde42bec04 100644
--- a/osu.Game.Rulesets.Taiko/UI/DrumRollHitContainer.cs
+++ b/osu.Game.Rulesets.Taiko/UI/DrumRollHitContainer.cs
@@ -2,7 +2,6 @@
 // See the LICENCE file in the repository root for full licence text.
 
 using osu.Framework.Graphics.Containers;
-using osu.Framework.Graphics.Performance;
 using osu.Game.Rulesets.Objects.Drawables;
 using osu.Game.Rulesets.Taiko.Objects.Drawables;
 using osu.Game.Rulesets.UI.Scrolling;
diff --git a/osu.Game/Rulesets/UI/HitObjectContainer.cs b/osu.Game/Rulesets/UI/HitObjectContainer.cs
index 4cadfa9ad4..9a0217a1eb 100644
--- a/osu.Game/Rulesets/UI/HitObjectContainer.cs
+++ b/osu.Game/Rulesets/UI/HitObjectContainer.cs
@@ -6,7 +6,6 @@ using System.Linq;
 using osu.Framework.Bindables;
 using osu.Framework.Graphics;
 using osu.Framework.Graphics.Containers;
-using osu.Framework.Graphics.Performance;
 using osu.Game.Rulesets.Objects.Drawables;
 
 namespace osu.Game.Rulesets.UI

From 0c1d12460fcc0304ce1889e21c684da5f107f59d Mon Sep 17 00:00:00 2001
From: Dean Herbert <pe@ppy.sh>
Date: Fri, 30 Oct 2020 10:30:11 +0900
Subject: [PATCH 12/21] Remove unused parameter

---
 osu.Game.Tests/Visual/Ranking/TestSceneAccuracyCircle.cs        | 2 +-
 osu.Game/Screens/Ranking/Expanded/Accuracy/AccuracyCircle.cs    | 2 +-
 osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs | 2 +-
 3 files changed, 3 insertions(+), 3 deletions(-)

diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneAccuracyCircle.cs b/osu.Game.Tests/Visual/Ranking/TestSceneAccuracyCircle.cs
index f69ccc1773..1e87893f39 100644
--- a/osu.Game.Tests/Visual/Ranking/TestSceneAccuracyCircle.cs
+++ b/osu.Game.Tests/Visual/Ranking/TestSceneAccuracyCircle.cs
@@ -120,7 +120,7 @@ namespace osu.Game.Tests.Visual.Ranking
                         }
                     }
                 },
-                new AccuracyCircle(score, false)
+                new AccuracyCircle(score)
                 {
                     Anchor = Anchor.Centre,
                     Origin = Anchor.Centre,
diff --git a/osu.Game/Screens/Ranking/Expanded/Accuracy/AccuracyCircle.cs b/osu.Game/Screens/Ranking/Expanded/Accuracy/AccuracyCircle.cs
index 337665b51f..45da23f1f9 100644
--- a/osu.Game/Screens/Ranking/Expanded/Accuracy/AccuracyCircle.cs
+++ b/osu.Game/Screens/Ranking/Expanded/Accuracy/AccuracyCircle.cs
@@ -78,7 +78,7 @@ namespace osu.Game.Screens.Ranking.Expanded.Accuracy
         private Container<RankBadge> badges;
         private RankText rankText;
 
-        public AccuracyCircle(ScoreInfo score, bool withFlair)
+        public AccuracyCircle(ScoreInfo score)
         {
             this.score = score;
         }
diff --git a/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs b/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs
index 711763330c..cb4560802b 100644
--- a/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs
+++ b/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs
@@ -120,7 +120,7 @@ namespace osu.Game.Screens.Ranking.Expanded
                                     Margin = new MarginPadding { Top = 40 },
                                     RelativeSizeAxes = Axes.X,
                                     Height = 230,
-                                    Child = new AccuracyCircle(score, withFlair)
+                                    Child = new AccuracyCircle(score)
                                     {
                                         Anchor = Anchor.Centre,
                                         Origin = Anchor.Centre,

From 46d89d55f4e9b33edc97cb3696cf2a9ebcee7727 Mon Sep 17 00:00:00 2001
From: Dean Herbert <pe@ppy.sh>
Date: Fri, 30 Oct 2020 12:46:48 +0900
Subject: [PATCH 13/21] Add note about ScheduleAfterChildren requirement

---
 osu.Game/Screens/Ranking/ScorePanelList.cs | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/osu.Game/Screens/Ranking/ScorePanelList.cs b/osu.Game/Screens/Ranking/ScorePanelList.cs
index 4325d317c4..77b3d8fc3b 100644
--- a/osu.Game/Screens/Ranking/ScorePanelList.cs
+++ b/osu.Game/Screens/Ranking/ScorePanelList.cs
@@ -172,7 +172,8 @@ namespace osu.Game.Screens.Ranking
             expandedTrackingComponent.Margin = new MarginPadding { Horizontal = expanded_panel_spacing };
             expandedPanel.State = PanelState.Expanded;
 
-            SchedulerAfterChildren.Add(() =>
+            // requires schedule after children to ensure the flow (and thus ScrollContainer's ScrollableExtent) has been updated.
+            ScheduleAfterChildren(() =>
             {
                 // Scroll to the new panel. This is done manually since we need:
                 // 1) To scroll after the scroll container's visible range is updated.

From 18f92818daed770f18d35fc74cc22c4da392e567 Mon Sep 17 00:00:00 2001
From: Dean Herbert <pe@ppy.sh>
Date: Fri, 30 Oct 2020 13:09:13 +0900
Subject: [PATCH 14/21] Show current HUD visibility mode as a tracked setting

---
 osu.Game/Configuration/OsuConfigManager.cs | 1 +
 1 file changed, 1 insertion(+)

diff --git a/osu.Game/Configuration/OsuConfigManager.cs b/osu.Game/Configuration/OsuConfigManager.cs
index 7d601c0cb9..46c5e61784 100644
--- a/osu.Game/Configuration/OsuConfigManager.cs
+++ b/osu.Game/Configuration/OsuConfigManager.cs
@@ -170,6 +170,7 @@ namespace osu.Game.Configuration
         public override TrackedSettings CreateTrackedSettings() => new TrackedSettings
         {
             new TrackedSetting<bool>(OsuSetting.MouseDisableButtons, v => new SettingDescription(!v, "gameplay mouse buttons", v ? "disabled" : "enabled")),
+            new TrackedSetting<HUDVisibilityMode>(OsuSetting.HUDVisibilityMode, m => new SettingDescription(m, "HUD Visibility", m.GetDescription())),
             new TrackedSetting<ScalingMode>(OsuSetting.Scaling, m => new SettingDescription(m, "scaling", m.GetDescription())),
         };
     }

From 9bb86ccb832d8f838517c5d4ef7af8d018d2ed38 Mon Sep 17 00:00:00 2001
From: Dean Herbert <pe@ppy.sh>
Date: Fri, 30 Oct 2020 13:09:22 +0900
Subject: [PATCH 15/21] Change shift-tab to cycle available HUD visibility
 modes

---
 osu.Game/Screens/Play/HUDOverlay.cs | 22 +++++++++++++++++++---
 1 file changed, 19 insertions(+), 3 deletions(-)

diff --git a/osu.Game/Screens/Play/HUDOverlay.cs b/osu.Game/Screens/Play/HUDOverlay.cs
index b047d44f8a..623041d9ca 100644
--- a/osu.Game/Screens/Play/HUDOverlay.cs
+++ b/osu.Game/Screens/Play/HUDOverlay.cs
@@ -277,9 +277,25 @@ namespace osu.Game.Screens.Play
                 switch (e.Key)
                 {
                     case Key.Tab:
-                        configVisibilityMode.Value = configVisibilityMode.Value != HUDVisibilityMode.Never
-                            ? HUDVisibilityMode.Never
-                            : HUDVisibilityMode.HideDuringGameplay;
+                        switch (configVisibilityMode.Value)
+                        {
+                            case HUDVisibilityMode.Never:
+                                configVisibilityMode.Value = HUDVisibilityMode.HideDuringGameplay;
+                                break;
+
+                            case HUDVisibilityMode.HideDuringGameplay:
+                                configVisibilityMode.Value = HUDVisibilityMode.HideDuringBreaks;
+                                break;
+
+                            case HUDVisibilityMode.HideDuringBreaks:
+                                configVisibilityMode.Value = HUDVisibilityMode.Always;
+                                break;
+
+                            case HUDVisibilityMode.Always:
+                                configVisibilityMode.Value = HUDVisibilityMode.Never;
+                                break;
+                        }
+
                         return true;
                 }
             }

From c72017a7db4ad00ba2b63d976fca20c5ea9ac583 Mon Sep 17 00:00:00 2001
From: Dean Herbert <pe@ppy.sh>
Date: Fri, 30 Oct 2020 13:49:44 +0900
Subject: [PATCH 16/21] Remove "hide during breaks" option

Probably wouldn't be used anyway.
---
 osu.Game/Configuration/HUDVisibilityMode.cs | 3 ---
 1 file changed, 3 deletions(-)

diff --git a/osu.Game/Configuration/HUDVisibilityMode.cs b/osu.Game/Configuration/HUDVisibilityMode.cs
index b0b55dd811..10f3f65355 100644
--- a/osu.Game/Configuration/HUDVisibilityMode.cs
+++ b/osu.Game/Configuration/HUDVisibilityMode.cs
@@ -12,9 +12,6 @@ namespace osu.Game.Configuration
         [Description("Hide during gameplay")]
         HideDuringGameplay,
 
-        [Description("Hide during breaks")]
-        HideDuringBreaks,
-
         Always
     }
 }

From b4eda65383cf80c56ac4991887836a12bb5a5be8 Mon Sep 17 00:00:00 2001
From: Dean Herbert <pe@ppy.sh>
Date: Fri, 30 Oct 2020 13:53:51 +0900
Subject: [PATCH 17/21] Commit missing pieces

---
 osu.Game/Screens/Play/HUDOverlay.cs | 9 ---------
 1 file changed, 9 deletions(-)

diff --git a/osu.Game/Screens/Play/HUDOverlay.cs b/osu.Game/Screens/Play/HUDOverlay.cs
index 623041d9ca..0cfe6effc1 100644
--- a/osu.Game/Screens/Play/HUDOverlay.cs
+++ b/osu.Game/Screens/Play/HUDOverlay.cs
@@ -223,11 +223,6 @@ namespace osu.Game.Screens.Play
                     ShowHud.Value = false;
                     break;
 
-                case HUDVisibilityMode.HideDuringBreaks:
-                    // always show during replay as we want the seek bar to be visible.
-                    ShowHud.Value = replayLoaded.Value || !IsBreakTime.Value;
-                    break;
-
                 case HUDVisibilityMode.HideDuringGameplay:
                     // always show during replay as we want the seek bar to be visible.
                     ShowHud.Value = replayLoaded.Value || IsBreakTime.Value;
@@ -284,10 +279,6 @@ namespace osu.Game.Screens.Play
                                 break;
 
                             case HUDVisibilityMode.HideDuringGameplay:
-                                configVisibilityMode.Value = HUDVisibilityMode.HideDuringBreaks;
-                                break;
-
-                            case HUDVisibilityMode.HideDuringBreaks:
                                 configVisibilityMode.Value = HUDVisibilityMode.Always;
                                 break;
 

From 53bd31c69e6acada773346e350ffc12430ae651d Mon Sep 17 00:00:00 2001
From: Dean Herbert <pe@ppy.sh>
Date: Fri, 30 Oct 2020 14:00:07 +0900
Subject: [PATCH 18/21] Commit missing test pieces

---
 osu.Game.Tests/Visual/Gameplay/TestSceneHUDOverlay.cs | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneHUDOverlay.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneHUDOverlay.cs
index 6ec673704c..6764501569 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestSceneHUDOverlay.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneHUDOverlay.cs
@@ -89,7 +89,7 @@ namespace osu.Game.Tests.Visual.Gameplay
         [Test]
         public void TestExternalHideDoesntAffectConfig()
         {
-            HUDVisibilityMode originalConfigValue = HUDVisibilityMode.HideDuringBreaks;
+            HUDVisibilityMode originalConfigValue = HUDVisibilityMode.HideDuringGameplay;
 
             createNew();
 

From 8928aa6d92990ce761c205e80ac3c20b1a4feffe Mon Sep 17 00:00:00 2001
From: Dean Herbert <pe@ppy.sh>
Date: Fri, 30 Oct 2020 14:19:40 +0900
Subject: [PATCH 19/21] Add key binding to show HUD while held

---
 .../Input/Bindings/GlobalActionContainer.cs   |  4 +++
 osu.Game/Screens/Play/HUDOverlay.cs           | 36 ++++++++++++++++++-
 2 files changed, 39 insertions(+), 1 deletion(-)

diff --git a/osu.Game/Input/Bindings/GlobalActionContainer.cs b/osu.Game/Input/Bindings/GlobalActionContainer.cs
index 41be4cfcc3..3de4bb1f9d 100644
--- a/osu.Game/Input/Bindings/GlobalActionContainer.cs
+++ b/osu.Game/Input/Bindings/GlobalActionContainer.cs
@@ -67,6 +67,7 @@ namespace osu.Game.Input.Bindings
             new KeyBinding(new[] { InputKey.Control, InputKey.Plus }, GlobalAction.IncreaseScrollSpeed),
             new KeyBinding(new[] { InputKey.Control, InputKey.Minus }, GlobalAction.DecreaseScrollSpeed),
             new KeyBinding(InputKey.MouseMiddle, GlobalAction.PauseGameplay),
+            new KeyBinding(InputKey.Control, GlobalAction.HoldForHUD),
         };
 
         public IEnumerable<KeyBinding> AudioControlKeyBindings => new[]
@@ -187,5 +188,8 @@ namespace osu.Game.Input.Bindings
 
         [Description("Timing Mode")]
         EditorTimingMode,
+
+        [Description("Hold for HUD")]
+        HoldForHUD,
     }
 }
diff --git a/osu.Game/Screens/Play/HUDOverlay.cs b/osu.Game/Screens/Play/HUDOverlay.cs
index b047d44f8a..c38c2ee5f7 100644
--- a/osu.Game/Screens/Play/HUDOverlay.cs
+++ b/osu.Game/Screens/Play/HUDOverlay.cs
@@ -8,8 +8,10 @@ using osu.Framework.Bindables;
 using osu.Framework.Extensions.IEnumerableExtensions;
 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;
 using osu.Game.Overlays;
 using osu.Game.Overlays.Notifications;
 using osu.Game.Rulesets.Mods;
@@ -22,7 +24,7 @@ using osuTK.Input;
 namespace osu.Game.Screens.Play
 {
     [Cached]
-    public class HUDOverlay : Container
+    public class HUDOverlay : Container, IKeyBindingHandler<GlobalAction>
     {
         public const float FADE_DURATION = 400;
 
@@ -67,6 +69,8 @@ namespace osu.Game.Screens.Play
 
         internal readonly IBindable<bool> IsBreakTime = new Bindable<bool>();
 
+        private bool holdingForHUD;
+
         private IEnumerable<Drawable> hideTargets => new Drawable[] { visibilityContainer, KeyCounter };
 
         public HUDOverlay(ScoreProcessor scoreProcessor, HealthProcessor healthProcessor, DrawableRuleset drawableRuleset, IReadOnlyList<Mod> mods)
@@ -217,6 +221,12 @@ namespace osu.Game.Screens.Play
             if (ShowHud.Disabled)
                 return;
 
+            if (holdingForHUD)
+            {
+                ShowHud.Value = true;
+                return;
+            }
+
             switch (configVisibilityMode.Value)
             {
                 case HUDVisibilityMode.Never:
@@ -351,5 +361,29 @@ namespace osu.Game.Screens.Play
             HealthDisplay?.BindHealthProcessor(processor);
             FailingLayer?.BindHealthProcessor(processor);
         }
+
+        public bool OnPressed(GlobalAction action)
+        {
+            switch (action)
+            {
+                case GlobalAction.HoldForHUD:
+                    holdingForHUD = true;
+                    updateVisibility();
+                    return true;
+            }
+
+            return false;
+        }
+
+        public void OnReleased(GlobalAction action)
+        {
+            switch (action)
+            {
+                case GlobalAction.HoldForHUD:
+                    holdingForHUD = false;
+                    updateVisibility();
+                    break;
+            }
+        }
     }
 }

From bd7871d9f511b09ddd224759e3b9365f043c27d4 Mon Sep 17 00:00:00 2001
From: Dean Herbert <pe@ppy.sh>
Date: Fri, 30 Oct 2020 14:20:00 +0900
Subject: [PATCH 20/21] Update test scene to be non-skinnable (and add test
 covering momentary display)

---
 .../Visual/Gameplay/TestSceneHUDOverlay.cs    | 66 ++++++++++---------
 1 file changed, 34 insertions(+), 32 deletions(-)

diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneHUDOverlay.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneHUDOverlay.cs
index 6ec673704c..136c9e191d 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestSceneHUDOverlay.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneHUDOverlay.cs
@@ -2,29 +2,23 @@
 // 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.Extensions.IEnumerableExtensions;
 using osu.Framework.Graphics;
 using osu.Framework.Graphics.Containers;
 using osu.Framework.Testing;
 using osu.Game.Configuration;
-using osu.Game.Rulesets;
 using osu.Game.Rulesets.Mods;
-using osu.Game.Rulesets.Osu;
 using osu.Game.Screens.Play;
 using osuTK.Input;
 
 namespace osu.Game.Tests.Visual.Gameplay
 {
-    public class TestSceneHUDOverlay : SkinnableTestScene
+    public class TestSceneHUDOverlay : OsuManualInputManagerTestScene
     {
         private HUDOverlay hudOverlay;
 
-        private IEnumerable<HUDOverlay> hudOverlays => CreatedDrawables.OfType<HUDOverlay>();
-
         // best way to check without exposing.
         private Drawable hideTarget => hudOverlay.KeyCounter;
         private FillFlowContainer<KeyCounter> keyCounterFlow => hudOverlay.KeyCounter.ChildrenOfType<FillFlowContainer<KeyCounter>>().First();
@@ -37,17 +31,9 @@ namespace osu.Game.Tests.Visual.Gameplay
         {
             createNew();
 
-            AddRepeatStep("increase combo", () =>
-            {
-                foreach (var hud in hudOverlays)
-                    hud.ComboCounter.Current.Value++;
-            }, 10);
+            AddRepeatStep("increase combo", () => { hudOverlay.ComboCounter.Current.Value++; }, 10);
 
-            AddStep("reset combo", () =>
-            {
-                foreach (var hud in hudOverlays)
-                    hud.ComboCounter.Current.Value = 0;
-            });
+            AddStep("reset combo", () => { hudOverlay.ComboCounter.Current.Value = 0; });
         }
 
         [Test]
@@ -77,7 +63,7 @@ namespace osu.Game.Tests.Visual.Gameplay
         {
             createNew();
 
-            AddStep("set showhud false", () => hudOverlays.ForEach(h => h.ShowHud.Value = false));
+            AddStep("set showhud false", () => hudOverlay.ShowHud.Value = false);
 
             AddUntilStep("hidetarget is hidden", () => !hideTarget.IsPresent);
             AddAssert("pause button is still visible", () => hudOverlay.HoldToQuit.IsPresent);
@@ -86,6 +72,27 @@ namespace osu.Game.Tests.Visual.Gameplay
             AddAssert("key counter flow not affected", () => keyCounterFlow.IsPresent);
         }
 
+        [Test]
+        public void TestMomentaryShowHUD()
+        {
+            createNew();
+
+            HUDVisibilityMode originalConfigValue = HUDVisibilityMode.HideDuringBreaks;
+            AddStep("get original config value", () => originalConfigValue = config.Get<HUDVisibilityMode>(OsuSetting.HUDVisibilityMode));
+
+            AddStep("set hud to never show", () => config.Set(OsuSetting.HUDVisibilityMode, HUDVisibilityMode.Never));
+
+            AddUntilStep("wait for fade", () => !hideTarget.IsPresent);
+
+            AddStep("trigger momentary show", () => InputManager.PressKey(Key.ControlLeft));
+            AddUntilStep("wait for visible", () => hideTarget.IsPresent);
+
+            AddStep("stop trigering", () => InputManager.ReleaseKey(Key.ControlLeft));
+            AddUntilStep("wait for fade", () => !hideTarget.IsPresent);
+
+            AddStep("set original config value", () => config.Set(OsuSetting.HUDVisibilityMode, originalConfigValue));
+        }
+
         [Test]
         public void TestExternalHideDoesntAffectConfig()
         {
@@ -113,14 +120,14 @@ namespace osu.Game.Tests.Visual.Gameplay
             AddStep("set keycounter visible false", () =>
             {
                 config.Set<bool>(OsuSetting.KeyOverlay, false);
-                hudOverlays.ForEach(h => h.KeyCounter.AlwaysVisible.Value = false);
+                hudOverlay.KeyCounter.AlwaysVisible.Value = false;
             });
 
-            AddStep("set showhud false", () => hudOverlays.ForEach(h => h.ShowHud.Value = false));
+            AddStep("set showhud false", () => hudOverlay.ShowHud.Value = false);
             AddUntilStep("hidetarget is hidden", () => !hideTarget.IsPresent);
             AddAssert("key counters hidden", () => !keyCounterFlow.IsPresent);
 
-            AddStep("set showhud true", () => hudOverlays.ForEach(h => h.ShowHud.Value = true));
+            AddStep("set showhud true", () => hudOverlay.ShowHud.Value = true);
             AddUntilStep("hidetarget is visible", () => hideTarget.IsPresent);
             AddAssert("key counters still hidden", () => !keyCounterFlow.IsPresent);
 
@@ -131,22 +138,17 @@ namespace osu.Game.Tests.Visual.Gameplay
         {
             AddStep("create overlay", () =>
             {
-                SetContents(() =>
-                {
-                    hudOverlay = new HUDOverlay(null, null, null, Array.Empty<Mod>());
+                hudOverlay = new HUDOverlay(null, null, null, Array.Empty<Mod>());
 
-                    // Add any key just to display the key counter visually.
-                    hudOverlay.KeyCounter.Add(new KeyCounterKeyboard(Key.Space));
+                // Add any key just to display the key counter visually.
+                hudOverlay.KeyCounter.Add(new KeyCounterKeyboard(Key.Space));
 
-                    hudOverlay.ComboCounter.Current.Value = 1;
+                hudOverlay.ComboCounter.Current.Value = 1;
 
-                    action?.Invoke(hudOverlay);
+                action?.Invoke(hudOverlay);
 
-                    return hudOverlay;
-                });
+                Child = hudOverlay;
             });
         }
-
-        protected override Ruleset CreateRulesetForSkinProvider() => new OsuRuleset();
     }
 }

From 984a243eff796505ee9f2ca6e85ae2a00233e3a4 Mon Sep 17 00:00:00 2001
From: Dean Herbert <pe@ppy.sh>
Date: Fri, 30 Oct 2020 14:23:24 +0900
Subject: [PATCH 21/21] Add skinnable test scene for HUD overlay

---
 .../Gameplay/TestSceneSkinnableHUDOverlay.cs  | 99 +++++++++++++++++++
 1 file changed, 99 insertions(+)
 create mode 100644 osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableHUDOverlay.cs

diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableHUDOverlay.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableHUDOverlay.cs
new file mode 100644
index 0000000000..fec1610160
--- /dev/null
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableHUDOverlay.cs
@@ -0,0 +1,99 @@
+// 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.Extensions.IEnumerableExtensions;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Testing;
+using osu.Game.Configuration;
+using osu.Game.Rulesets;
+using osu.Game.Rulesets.Mods;
+using osu.Game.Rulesets.Osu;
+using osu.Game.Screens.Play;
+using osuTK.Input;
+
+namespace osu.Game.Tests.Visual.Gameplay
+{
+    public class TestSceneSkinnableHUDOverlay : SkinnableTestScene
+    {
+        private HUDOverlay hudOverlay;
+
+        private IEnumerable<HUDOverlay> hudOverlays => CreatedDrawables.OfType<HUDOverlay>();
+
+        // best way to check without exposing.
+        private Drawable hideTarget => hudOverlay.KeyCounter;
+        private FillFlowContainer<KeyCounter> keyCounterFlow => hudOverlay.KeyCounter.ChildrenOfType<FillFlowContainer<KeyCounter>>().First();
+
+        [Resolved]
+        private OsuConfigManager config { get; set; }
+
+        [Test]
+        public void TestComboCounterIncrementing()
+        {
+            createNew();
+
+            AddRepeatStep("increase combo", () =>
+            {
+                foreach (var hud in hudOverlays)
+                    hud.ComboCounter.Current.Value++;
+            }, 10);
+
+            AddStep("reset combo", () =>
+            {
+                foreach (var hud in hudOverlays)
+                    hud.ComboCounter.Current.Value = 0;
+            });
+        }
+
+        [Test]
+        public void TestFadesInOnLoadComplete()
+        {
+            float? initialAlpha = null;
+
+            createNew(h => h.OnLoadComplete += _ => initialAlpha = hideTarget.Alpha);
+            AddUntilStep("wait for load", () => hudOverlay.IsAlive);
+            AddAssert("initial alpha was less than 1", () => initialAlpha < 1);
+        }
+
+        [Test]
+        public void TestHideExternally()
+        {
+            createNew();
+
+            AddStep("set showhud false", () => hudOverlays.ForEach(h => h.ShowHud.Value = false));
+
+            AddUntilStep("hidetarget is hidden", () => !hideTarget.IsPresent);
+            AddAssert("pause button is still visible", () => hudOverlay.HoldToQuit.IsPresent);
+
+            // Key counter flow container should not be affected by this, only the key counter display will be hidden as checked above.
+            AddAssert("key counter flow not affected", () => keyCounterFlow.IsPresent);
+        }
+
+        private void createNew(Action<HUDOverlay> action = null)
+        {
+            AddStep("create overlay", () =>
+            {
+                SetContents(() =>
+                {
+                    hudOverlay = new HUDOverlay(null, null, null, Array.Empty<Mod>());
+
+                    // Add any key just to display the key counter visually.
+                    hudOverlay.KeyCounter.Add(new KeyCounterKeyboard(Key.Space));
+
+                    hudOverlay.ComboCounter.Current.Value = 1;
+
+                    action?.Invoke(hudOverlay);
+
+                    return hudOverlay;
+                });
+            });
+        }
+
+        protected override Ruleset CreateRulesetForSkinProvider() => new OsuRuleset();
+    }
+}