diff --git a/osu.Game.Tests/Visual/UserInterface/TestCaseButtonSystem.cs b/osu.Game.Tests/Visual/UserInterface/TestCaseButtonSystem.cs index 261e87ff07..04aa8bce7e 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestCaseButtonSystem.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestCaseButtonSystem.cs @@ -36,7 +36,7 @@ namespace osu.Game.Tests.Visual.UserInterface RelativeSizeAxes = Axes.Both, }, buttons = new ButtonSystem(), - logo = new OsuLogo() + logo = new OsuLogo { RelativePositionAxes = Axes.Both } }; buttons.SetOsuLogo(logo); diff --git a/osu.Game.Tests/Visual/UserInterface/TestCaseLogoTrackingContainer.cs b/osu.Game.Tests/Visual/UserInterface/TestCaseLogoTrackingContainer.cs new file mode 100644 index 0000000000..e45e2e24da --- /dev/null +++ b/osu.Game.Tests/Visual/UserInterface/TestCaseLogoTrackingContainer.cs @@ -0,0 +1,299 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Collections.Generic; +using NUnit.Framework; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.UserInterface; +using osu.Framework.MathUtils; +using osu.Framework.Testing; +using osu.Game.Graphics.Containers; +using osu.Game.Screens.Menu; +using osu.Game.Screens.Play; +using osuTK; +using osuTK.Graphics; + +namespace osu.Game.Tests.Visual.UserInterface +{ + public class TestCaseLogoTrackingContainer : OsuTestCase + { + public override IReadOnlyList RequiredTypes => new[] + { + typeof(PlayerLoader), + typeof(Player), + typeof(LogoTrackingContainer), + typeof(ButtonSystem), + typeof(ButtonSystemState), + typeof(Menu), + typeof(MainMenu) + }; + + private OsuLogo logo; + private TestLogoTrackingContainer trackingContainer; + private Container transferContainer; + private Box visualBox; + private Box transferContainerBox; + private Drawable logoFacade; + private bool randomPositions; + + private const float visual_box_size = 72; + + [SetUpSteps] + public void SetUpSteps() + { + AddStep("Clear facades", () => + { + Clear(); + Add(logo = new OsuLogo { Scale = new Vector2(0.15f), RelativePositionAxes = Axes.Both }); + trackingContainer = null; + transferContainer = null; + }); + } + + /// + /// Move the facade to 0,0, then move it to a random new location while the logo is still transforming to it. + /// Check if the logo is still tracking the facade. + /// + [Test] + public void TestMoveFacade() + { + AddToggleStep("Toggle move continuously", b => randomPositions = b); + AddStep("Add tracking containers", addFacadeContainers); + AddStep("Move facade to random position", moveLogoFacade); + waitForMove(); + AddAssert("Logo is tracking", () => trackingContainer.IsLogoTracking); + } + + /// + /// Check if the facade is removed from the container, the logo stops tracking. + /// + [Test] + public void TestRemoveFacade() + { + AddStep("Add tracking containers", addFacadeContainers); + AddStep("Move facade to random position", moveLogoFacade); + AddStep("Remove facade from FacadeContainer", removeFacade); + waitForMove(); + AddAssert("Logo is not tracking", () => !trackingContainer.IsLogoTracking); + } + + /// + /// Check if the facade gets added to a new container, tracking starts on the new facade. + /// + [Test] + public void TestTransferFacade() + { + AddStep("Add tracking containers", addFacadeContainers); + AddStep("Move facade to random position", moveLogoFacade); + AddStep("Remove facade from FacadeContainer", removeFacade); + AddStep("Transfer facade to a new container", () => + { + transferContainer.Add(logoFacade); + transferContainerBox.Colour = Color4.Tomato; + moveLogoFacade(); + }); + + waitForMove(); + AddAssert("Logo is tracking", () => trackingContainer.IsLogoTracking); + } + + /// + /// Add a facade to a flow container, move the logo to the center of the screen, then start tracking on the facade. + /// + [Test] + public void TestFlowContainer() + { + FillFlowContainer flowContainer; + + AddStep("Create new flow container with facade", () => + { + Add(trackingContainer = new TestLogoTrackingContainer + { + AutoSizeAxes = Axes.Both, + Origin = Anchor.TopCentre, + Anchor = Anchor.TopCentre, + Child = flowContainer = new FillFlowContainer + { + AutoSizeAxes = Axes.Both, + Origin = Anchor.TopCentre, + Anchor = Anchor.TopCentre, + Direction = FillDirection.Vertical, + } + }); + flowContainer.Children = new Drawable[] + { + new Box + { + Origin = Anchor.TopCentre, + Anchor = Anchor.TopCentre, + Colour = Color4.Azure, + Size = new Vector2(visual_box_size) + }, + new Container + { + Alpha = 0.35f, + RelativeSizeAxes = Axes.None, + Size = new Vector2(visual_box_size), + Origin = Anchor.TopCentre, + Anchor = Anchor.TopCentre, + Children = new Drawable[] + { + visualBox = new Box + { + Colour = Color4.White, + RelativeSizeAxes = Axes.Both, + }, + trackingContainer.LogoFacade, + } + }, + new Box + { + Origin = Anchor.TopCentre, + Anchor = Anchor.TopCentre, + Colour = Color4.Azure, + Size = new Vector2(visual_box_size) + }, + }; + }); + + AddStep("Perform logo movements", () => + { + trackingContainer.StopTracking(); + logo.MoveTo(new Vector2(0.5f), 500, Easing.InOutExpo); + + visualBox.Colour = Color4.White; + + Scheduler.AddDelayed(() => + { + trackingContainer.StartTracking(logo, 1000, Easing.InOutExpo); + visualBox.Colour = Color4.Tomato; + }, 700); + }); + + waitForMove(8); + AddAssert("Logo is tracking", () => trackingContainer.IsLogoTracking); + } + + [Test] + public void TestSetFacadeSize() + { + bool failed = false; + + AddStep("Set up scenario", () => + { + failed = false; + addFacadeContainers(); + }); + + AddStep("Try setting facade size", () => + { + try + { + logoFacade.Size = new Vector2(0, 0); + } + catch (Exception e) + { + if (e is InvalidOperationException) + failed = true; + } + }); + + AddAssert("Exception thrown", () => failed); + } + + [Test] + public void TestSetMultipleContainers() + { + bool failed = false; + LogoTrackingContainer newContainer = new LogoTrackingContainer(); + + AddStep("Set up scenario", () => + { + failed = false; + newContainer = new LogoTrackingContainer(); + addFacadeContainers(); + moveLogoFacade(); + }); + + AddStep("Try tracking new container", () => + { + try + { + newContainer.StartTracking(logo); + } + catch (Exception e) + { + if (e is InvalidOperationException) + failed = true; + } + }); + + AddAssert("Exception thrown", () => failed); + } + + private void addFacadeContainers() + { + AddRange(new Drawable[] + { + trackingContainer = new TestLogoTrackingContainer + { + Alpha = 0.35f, + RelativeSizeAxes = Axes.None, + Size = new Vector2(visual_box_size), + Child = visualBox = new Box + { + Colour = Color4.Tomato, + RelativeSizeAxes = Axes.Both, + } + }, + transferContainer = new Container + { + Alpha = 0.35f, + RelativeSizeAxes = Axes.None, + Size = new Vector2(visual_box_size), + Child = transferContainerBox = new Box + { + Colour = Color4.White, + RelativeSizeAxes = Axes.Both, + } + }, + }); + + trackingContainer.Add(logoFacade = trackingContainer.LogoFacade); + trackingContainer.StartTracking(logo, 1000); + } + + private void waitForMove(int count = 5) => AddWaitStep("Wait for transforms to finish", count); + + private void removeFacade() + { + trackingContainer.Remove(logoFacade); + visualBox.Colour = Color4.White; + moveLogoFacade(); + } + + private void moveLogoFacade() + { + if (logoFacade?.Transforms.Count == 0 && transferContainer?.Transforms.Count == 0) + { + Random random = new Random(); + trackingContainer.Delay(500).MoveTo(new Vector2(random.Next(0, (int)logo.Parent.DrawWidth), random.Next(0, (int)logo.Parent.DrawHeight)), 300); + transferContainer.Delay(500).MoveTo(new Vector2(random.Next(0, (int)logo.Parent.DrawWidth), random.Next(0, (int)logo.Parent.DrawHeight)), 300); + } + + if (randomPositions) + Schedule(moveLogoFacade); + } + + private class TestLogoTrackingContainer : LogoTrackingContainer + { + /// + /// Check that the logo is tracking the position of the facade, with an acceptable precision lenience. + /// + public bool IsLogoTracking => Precision.AlmostEquals(Logo.Position, ComputeLogoTrackingPosition()); + } + } +} diff --git a/osu.Game/Graphics/Containers/LogoTrackingContainer.cs b/osu.Game/Graphics/Containers/LogoTrackingContainer.cs new file mode 100644 index 0000000000..fb23038dde --- /dev/null +++ b/osu.Game/Graphics/Containers/LogoTrackingContainer.cs @@ -0,0 +1,158 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.MathUtils; +using osu.Game.Screens.Menu; +using osuTK; + +namespace osu.Game.Graphics.Containers +{ + /// + /// A container that handles tracking of an through different layout scenarios. + /// + public class LogoTrackingContainer : Container + { + public Facade LogoFacade => facade; + + protected OsuLogo Logo { get; private set; } + + private readonly InternalFacade facade = new InternalFacade(); + + private Easing easing; + private Vector2? startPosition; + private double? startTime; + private double duration; + + /// + /// Assign the logo that should track the facade's position, as well as how it should transform to its initial position. + /// + /// The instance of the logo to be used for tracking. + /// The scale of the facade. Does not actually affect the logo itself. + /// The duration of the initial transform. Default is instant. + /// The easing type of the initial transform. + public void StartTracking(OsuLogo logo, double duration = 0, Easing easing = Easing.None) + { + if (logo == null) + throw new ArgumentNullException(nameof(logo)); + + if (logo.IsTracking && Logo == null) + throw new InvalidOperationException($"Cannot track an instance of {typeof(OsuLogo)} to multiple {typeof(LogoTrackingContainer)}s"); + + if (Logo != logo && Logo != null) + { + // If we're replacing the logo to be tracked, the old one no longer has a tracking container + Logo.IsTracking = false; + } + + Logo = logo; + Logo.IsTracking = true; + + this.duration = duration; + this.easing = easing; + + startTime = null; + startPosition = null; + } + + /// + /// Stops the logo assigned in from tracking the facade's position. + /// + public void StopTracking() + { + if (Logo != null) + { + Logo.IsTracking = false; + Logo = null; + } + } + + /// + /// Gets the position that the logo should move to with respect to the . + /// Manually performs a conversion of the Facade's position to the Logo's parent's relative space. + /// + /// Will only be correct if the logo's are set to Axes.Both + protected Vector2 ComputeLogoTrackingPosition() + { + var absolutePos = Logo.Parent.ToLocalSpace(LogoFacade.ScreenSpaceDrawQuad.Centre); + + return new Vector2(absolutePos.X / Logo.Parent.RelativeToAbsoluteFactor.X, + absolutePos.Y / Logo.Parent.RelativeToAbsoluteFactor.Y); + } + + protected override void Update() + { + base.Update(); + + if (Logo == null) + return; + + if (Logo.RelativePositionAxes != Axes.Both) + throw new InvalidOperationException($"Tracking logo must have {nameof(RelativePositionAxes)} = Axes.Both"); + + // Account for the scale of the actual OsuLogo, as SizeForFlow only accounts for the sprite scale. + facade.SetSize(new Vector2(Logo.SizeForFlow * Logo.Scale.X)); + + var localPos = ComputeLogoTrackingPosition(); + + if (LogoFacade.Parent != null && Logo.Position != localPos) + { + // If this is our first update since tracking has started, initialize our starting values for interpolation + if (startTime == null || startPosition == null) + { + startTime = Time.Current; + startPosition = Logo.Position; + } + + if (duration != 0) + { + double elapsedDuration = (double)(Time.Current - startTime); + + var amount = (float)Interpolation.ApplyEasing(easing, Math.Min(elapsedDuration / duration, 1)); + + // Interpolate the position of the logo, where amount 0 is where the logo was when it first began interpolating, and amount 1 is the target location. + Logo.Position = Vector2.Lerp(startPosition.Value, localPos, amount); + } + else + { + Logo.Position = localPos; + } + } + } + + protected override void Dispose(bool isDisposing) + { + if (Logo != null) + Logo.IsTracking = false; + + base.Dispose(isDisposing); + } + + private class InternalFacade : Facade + { + public void SetSize(Vector2 size) + { + base.SetSize(size); + } + } + + /// + /// A dummy object used to denote another object's location. + /// + public abstract class Facade : Drawable + { + public override Vector2 Size + { + get => base.Size; + set => throw new InvalidOperationException($"Cannot set the Size of a {typeof(Facade)} outside of a {typeof(LogoTrackingContainer)}"); + } + + protected void SetSize(Vector2 size) + { + base.Size = size; + } + } + } +} diff --git a/osu.Game/Screens/Menu/ButtonArea.cs b/osu.Game/Screens/Menu/ButtonArea.cs index d6e1aef63c..eada1e0777 100644 --- a/osu.Game/Screens/Menu/ButtonArea.cs +++ b/osu.Game/Screens/Menu/ButtonArea.cs @@ -32,6 +32,7 @@ namespace osu.Game.Screens.Menu RelativeSizeAxes = Axes.X, Size = new Vector2(1, BUTTON_AREA_HEIGHT), Alpha = 0, + AlwaysPresent = true, // Always needs to be present for correct tracking on initial -> toplevel state change Children = new Drawable[] { buttonAreaBackground = new ButtonAreaBackground(), diff --git a/osu.Game/Screens/Menu/ButtonSystem.cs b/osu.Game/Screens/Menu/ButtonSystem.cs index 80fbce2cf8..519fadb34b 100644 --- a/osu.Game/Screens/Menu/ButtonSystem.cs +++ b/osu.Game/Screens/Menu/ButtonSystem.cs @@ -9,6 +9,7 @@ using osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.Audio.Sample; using osu.Framework.Bindables; +using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Sprites; @@ -17,6 +18,7 @@ using osu.Framework.Logging; using osu.Framework.Platform; using osu.Framework.Threading; using osu.Game.Graphics; +using osu.Game.Graphics.Containers; using osu.Game.Input; using osu.Game.Input.Bindings; using osu.Game.Online.API; @@ -47,6 +49,10 @@ namespace osu.Game.Screens.Menu private OsuLogo logo; + /// + /// Assign the that this ButtonSystem should manage the position of. + /// + /// The instance of the logo to be assigned. If null, we are suspending from the screen that uses this ButtonSystem. public void SetOsuLogo(OsuLogo logo) { this.logo = logo; @@ -60,9 +66,13 @@ namespace osu.Game.Screens.Menu updateLogoState(); } + else + { + // We should stop tracking as the facade is now out of scope. + logoTrackingContainer.StopTracking(); + } } - private readonly Drawable iconFacade; private readonly ButtonArea buttonArea; private readonly Button backButton; @@ -72,26 +82,29 @@ namespace osu.Game.Screens.Menu private SampleChannel sampleBack; + private readonly LogoTrackingContainer logoTrackingContainer; + public ButtonSystem() { RelativeSizeAxes = Axes.Both; - Child = buttonArea = new ButtonArea(); + Child = logoTrackingContainer = new LogoTrackingContainer + { + RelativeSizeAxes = Axes.Both, + Child = buttonArea = new ButtonArea() + }; - buttonArea.AddRange(new[] + buttonArea.AddRange(new Drawable[] { new Button(@"settings", string.Empty, FontAwesome.Solid.Cog, new Color4(85, 85, 85, 255), () => OnSettings?.Invoke(), -WEDGE_WIDTH, Key.O), backButton = new Button(@"back", @"button-back-select", OsuIcon.LeftCircle, new Color4(51, 58, 94, 255), () => State = ButtonSystemState.TopLevel, -WEDGE_WIDTH) { VisibleState = ButtonSystemState.Play, }, - iconFacade = new Container //need a container to make the osu! icon flow properly. - { - Size = new Vector2(0, ButtonArea.BUTTON_AREA_HEIGHT) - } + logoTrackingContainer.LogoFacade.With(d => d.Scale = new Vector2(0.74f)) }); - buttonArea.Flow.CentreTarget = iconFacade; + buttonArea.Flow.CentreTarget = logoTrackingContainer.LogoFacade; } [Resolved(CanBeNull = true)] @@ -124,6 +137,15 @@ namespace osu.Game.Screens.Menu buttonArea.AddRange(buttonsPlay); buttonArea.AddRange(buttonsTopLevel); + buttonArea.ForEach(b => + { + if (b is Button) + { + b.Origin = Anchor.CentreLeft; + b.Anchor = Anchor.CentreLeft; + } + }); + isIdle.ValueChanged += idle => updateIdleState(idle.NewValue); if (idleTracker != null) isIdle.BindTo(idleTracker.IsIdle); @@ -256,13 +278,11 @@ namespace osu.Game.Screens.Menu logoDelayedAction?.Cancel(); logoDelayedAction = Scheduler.AddDelayed(() => { - logoTracking = false; + logoTrackingContainer.StopTracking(); game?.Toolbar.Hide(); logo.ClearTransforms(targetMember: nameof(Position)); - logo.RelativePositionAxes = Axes.Both; - logo.MoveTo(new Vector2(0.5f), 800, Easing.OutExpo); logo.ScaleTo(1, 800, Easing.OutExpo); }, buttonArea.Alpha * 150); @@ -275,20 +295,17 @@ namespace osu.Game.Screens.Menu break; case ButtonSystemState.Initial: logo.ClearTransforms(targetMember: nameof(Position)); - logo.RelativePositionAxes = Axes.None; bool impact = logo.Scale.X > 0.6f; if (lastState == ButtonSystemState.Initial) logo.ScaleTo(0.5f, 200, Easing.In); - logo.MoveTo(logoTrackingPosition, lastState == ButtonSystemState.EnteringMode ? 0 : 200, Easing.In); + logoTrackingContainer.StartTracking(logo, lastState == ButtonSystemState.EnteringMode ? 0 : 200, Easing.In); logoDelayedAction?.Cancel(); logoDelayedAction = Scheduler.AddDelayed(() => { - logoTracking = true; - if (impact) logo.Impact(); @@ -297,35 +314,17 @@ namespace osu.Game.Screens.Menu break; default: logo.ClearTransforms(targetMember: nameof(Position)); - logo.RelativePositionAxes = Axes.None; - logoTracking = true; + logoTrackingContainer.StartTracking(logo, 0, Easing.In); logo.ScaleTo(0.5f, 200, Easing.OutQuint); break; } break; case ButtonSystemState.EnteringMode: - logoTracking = true; + logoTrackingContainer.StartTracking(logo, 0, Easing.In); break; } } - - private Vector2 logoTrackingPosition => logo.Parent.ToLocalSpace(iconFacade.ScreenSpaceDrawQuad.Centre); - - private bool logoTracking; - - protected override void Update() - { - base.Update(); - - if (logo != null) - { - if (logoTracking && logo.RelativePositionAxes == Axes.None && iconFacade.IsLoaded) - logo.Position = logoTrackingPosition; - - iconFacade.Width = logo.SizeForFlow * 0.5f; - } - } } public enum ButtonSystemState diff --git a/osu.Game/Screens/Menu/FlowContainerWithOrigin.cs b/osu.Game/Screens/Menu/FlowContainerWithOrigin.cs index ec7333ec02..8310ab06eb 100644 --- a/osu.Game/Screens/Menu/FlowContainerWithOrigin.cs +++ b/osu.Game/Screens/Menu/FlowContainerWithOrigin.cs @@ -29,7 +29,7 @@ namespace osu.Game.Screens.Menu if (CentreTarget == null) return base.OriginPosition; - return CentreTarget.DrawPosition + CentreTarget.DrawSize / 2; + return CentreTarget.DrawPosition + CentreTarget.DrawSize / 2 * CentreTarget.Scale; } } } diff --git a/osu.Game/Screens/Menu/OsuLogo.cs b/osu.Game/Screens/Menu/OsuLogo.cs index af697d37bd..4631f4e222 100644 --- a/osu.Game/Screens/Menu/OsuLogo.cs +++ b/osu.Game/Screens/Menu/OsuLogo.cs @@ -54,7 +54,13 @@ namespace osu.Game.Screens.Menu /// public Func Action; - public float SizeForFlow => logo == null ? 0 : logo.DrawSize.X * logo.Scale.X * logoBounceContainer.Scale.X * logoHoverContainer.Scale.X * 0.74f; + /// + /// The size of the logo Sprite with respect to the scale of its hover and bounce containers. + /// + /// Does not account for the scale of this + public float SizeForFlow => logo == null ? 0 : logo.DrawSize.X * logo.Scale.X * logoBounceContainer.Scale.X * logoHoverContainer.Scale.X; + + public bool IsTracking { get; set; } private readonly Sprite ripple; diff --git a/osu.Game/Screens/Play/PlayerLoader.cs b/osu.Game/Screens/Play/PlayerLoader.cs index 1c558eae2e..6a55fe278b 100644 --- a/osu.Game/Screens/Play/PlayerLoader.cs +++ b/osu.Game/Screens/Play/PlayerLoader.cs @@ -15,6 +15,7 @@ using osu.Framework.Screens; using osu.Framework.Threading; using osu.Game.Beatmaps; using osu.Game.Graphics; +using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; using osu.Game.Rulesets.Mods; @@ -34,7 +35,7 @@ namespace osu.Game.Screens.Play private Player player; - private Container content; + private LogoTrackingContainer content; private BeatmapMetadataDisplay info; @@ -61,35 +62,34 @@ namespace osu.Game.Screens.Play [BackgroundDependencyLoader] private void load() { - InternalChild = content = new Container + InternalChild = (content = new LogoTrackingContainer { Anchor = Anchor.Centre, Origin = Anchor.Centre, RelativeSizeAxes = Axes.Both, - Children = new Drawable[] + }).WithChildren(new Drawable[] + { + info = new BeatmapMetadataDisplay(Beatmap.Value, Mods.Value, content.LogoFacade) { - info = new BeatmapMetadataDisplay(Beatmap.Value, Mods.Value) + Alpha = 0, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + }, + new FillFlowContainer + { + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight, + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Vertical, + Spacing = new Vector2(0, 20), + Margin = new MarginPadding(25), + Children = new PlayerSettingsGroup[] { - Alpha = 0, - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - }, - new FillFlowContainer - { - Anchor = Anchor.TopRight, - Origin = Anchor.TopRight, - AutoSizeAxes = Axes.Both, - Direction = FillDirection.Vertical, - Spacing = new Vector2(0, 20), - Margin = new MarginPadding(25), - Children = new PlayerSettingsGroup[] - { - VisualSettings = new VisualSettings(), - new InputSettings() - } + VisualSettings = new VisualSettings(), + new InputSettings() } } - }; + }); loadNewPlayer(); } @@ -129,6 +129,9 @@ namespace osu.Game.Screens.Play private void contentOut() { + // Ensure the logo is no longer tracking before we scale the content + content.StopTracking(); + content.ScaleTo(0.7f, 300, Easing.InQuint); content.FadeOut(250); } @@ -150,11 +153,23 @@ namespace osu.Game.Screens.Play { base.LogoArriving(logo, resuming); - logo.ScaleTo(new Vector2(0.15f), 300, Easing.In); - logo.MoveTo(new Vector2(0.5f), 300, Easing.In); + const double duration = 300; + + if (!resuming) + { + logo.MoveTo(new Vector2(0.5f), duration, Easing.In); + } + + logo.ScaleTo(new Vector2(0.15f), duration, Easing.In); logo.FadeIn(350); - logo.Delay(resuming ? 0 : 500).MoveToOffset(new Vector2(0, -0.24f), 500, Easing.InOutExpo); + Scheduler.AddDelayed(() => { content.StartTracking(logo, resuming ? 0 : 500, Easing.InOutExpo); }, resuming ? 0 : 500); + } + + protected override void LogoExiting(OsuLogo logo) + { + base.LogoExiting(logo); + content.StopTracking(); } protected override void LoadComplete() @@ -166,7 +181,7 @@ namespace osu.Game.Screens.Play private ScheduledDelegate pushDebounce; protected VisualSettings VisualSettings; - // Hhere because IsHovered will not update unless we do so. + // Here because IsHovered will not update unless we do so. public override bool HandlePositionalInput => true; private bool readyForPush => player.LoadState == LoadState.Ready && IsHovered && GetContainingInputManager()?.DraggedDrawable == null; @@ -302,6 +317,7 @@ namespace osu.Game.Screens.Play private readonly WorkingBeatmap beatmap; private readonly IReadOnlyList mods; + private readonly Drawable facade; private LoadingAnimation loading; private Sprite backgroundSprite; private ModDisplay modDisplay; @@ -323,10 +339,11 @@ namespace osu.Game.Screens.Play } } - public BeatmapMetadataDisplay(WorkingBeatmap beatmap, IReadOnlyList mods) + public BeatmapMetadataDisplay(WorkingBeatmap beatmap, IReadOnlyList mods, Drawable facade) { this.beatmap = beatmap; this.mods = mods; + this.facade = facade; } [BackgroundDependencyLoader] @@ -343,14 +360,20 @@ namespace osu.Game.Screens.Play Origin = Anchor.TopCentre, Anchor = Anchor.TopCentre, Direction = FillDirection.Vertical, - Children = new Drawable[] + Children = new[] { + facade.With(d => + { + d.Anchor = Anchor.TopCentre; + d.Origin = Anchor.TopCentre; + }), new OsuSpriteText { Text = new LocalisedString((metadata.TitleUnicode, metadata.Title)), Font = OsuFont.GetFont(size: 36, italics: true), Origin = Anchor.TopCentre, Anchor = Anchor.TopCentre, + Margin = new MarginPadding { Top = 15 }, }, new OsuSpriteText {