// 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. #nullable disable using System; using osu.Framework; using osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.Bindables; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Audio; using osu.Framework.Graphics.Colour; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Framework.Input.Events; using osu.Framework.Utils; using osu.Game.Scoring; using osu.Game.Screens.Ranking.Contracted; using osu.Game.Screens.Ranking.Expanded; using osu.Game.Users; using osuTK; using osuTK.Graphics; namespace osu.Game.Screens.Ranking { public class ScorePanel : CompositeDrawable, IStateful<PanelState> { /// <summary> /// Width of the panel when contracted. /// </summary> public const float CONTRACTED_WIDTH = 130; /// <summary> /// Height of the panel when contracted. /// </summary> private const float contracted_height = 385; /// <summary> /// Width of the panel when expanded. /// </summary> public const float EXPANDED_WIDTH = 360; /// <summary> /// Height of the panel when expanded. /// </summary> private const float expanded_height = 586; /// <summary> /// Height of the top layer when the panel is expanded. /// </summary> private const float expanded_top_layer_height = 53; /// <summary> /// Height of the top layer when the panel is contracted. /// </summary> private const float contracted_top_layer_height = 30; /// <summary> /// Duration for the panel to resize into its expanded/contracted size. /// </summary> public const double RESIZE_DURATION = 200; /// <summary> /// Delay after <see cref="RESIZE_DURATION"/> before the top layer is expanded. /// </summary> public const double TOP_LAYER_EXPAND_DELAY = 100; /// <summary> /// Duration for the top layer expansion. /// </summary> private const double top_layer_expand_duration = 200; /// <summary> /// Duration for the panel contents to fade in. /// </summary> private const double content_fade_duration = 50; private static readonly ColourInfo expanded_top_layer_colour = ColourInfo.GradientVertical(Color4Extensions.FromHex("#444"), Color4Extensions.FromHex("#333")); private static readonly ColourInfo expanded_middle_layer_colour = ColourInfo.GradientVertical(Color4Extensions.FromHex("#555"), Color4Extensions.FromHex("#333")); private static readonly Color4 contracted_top_layer_colour = Color4Extensions.FromHex("#353535"); private static readonly Color4 contracted_middle_layer_colour = Color4Extensions.FromHex("#353535"); public event Action<PanelState> StateChanged; /// <summary> /// The position of the score in the rankings. /// </summary> public readonly Bindable<int?> ScorePosition = new Bindable<int?>(); /// <summary> /// An action to be invoked if this <see cref="ScorePanel"/> is clicked while in an expanded state. /// </summary> public Action PostExpandAction; public readonly ScoreInfo Score; [Resolved] private OsuGameBase game { get; set; } private AudioContainer audioContent; private bool displayWithFlair; private Container topLayerContainer; private Drawable topLayerBackground; private Container topLayerContentContainer; private Drawable topLayerContent; private Container middleLayerContainer; private Drawable middleLayerBackground; private Container middleLayerContentContainer; private Drawable middleLayerContent; private DrawableSample samplePanelFocus; public ScorePanel(ScoreInfo score, bool isNewLocalScore = false) { Score = score; displayWithFlair = isNewLocalScore; ScorePosition.Value = score.Position; } [BackgroundDependencyLoader] private void load(AudioManager audio) { // ScorePanel doesn't include the top extruding area in its own size. // Adding a manual offset here allows the expanded version to take on an "acceptable" vertical centre when at 100% UI scale. const float vertical_fudge = 20; InternalChild = audioContent = new AudioContainer { Anchor = Anchor.Centre, Origin = Anchor.Centre, Size = new Vector2(40), Y = vertical_fudge, Children = new Drawable[] { topLayerContainer = new Container { Name = "Top layer", RelativeSizeAxes = Axes.X, Alpha = 0, Height = 120, Children = new Drawable[] { new Container { RelativeSizeAxes = Axes.Both, CornerRadius = 20, CornerExponent = 2.5f, Masking = true, Child = topLayerBackground = new Box { RelativeSizeAxes = Axes.Both } }, topLayerContentContainer = new Container { RelativeSizeAxes = Axes.Both } } }, middleLayerContainer = new Container { Name = "Middle layer", RelativeSizeAxes = Axes.Both, Children = new Drawable[] { new Container { RelativeSizeAxes = Axes.Both, CornerRadius = 20, CornerExponent = 2.5f, Masking = true, Children = new[] { middleLayerBackground = new Box { RelativeSizeAxes = Axes.Both }, new UserCoverBackground { RelativeSizeAxes = Axes.Both, User = Score.User, Colour = ColourInfo.GradientVertical(Color4.White.Opacity(0.5f), Color4Extensions.FromHex("#444").Opacity(0)) } } }, middleLayerContentContainer = new Container { RelativeSizeAxes = Axes.Both } } }, samplePanelFocus = new DrawableSample(audio.Samples.Get(@"Results/score-panel-focus")) } }; } protected override void LoadComplete() { base.LoadComplete(); updateState(); topLayerBackground.FinishTransforms(false, nameof(Colour)); middleLayerBackground.FinishTransforms(false, nameof(Colour)); } private PanelState state = PanelState.Contracted; public PanelState State { get => state; set { if (state == value) return; state = value; if (IsLoaded) { updateState(); if (value == PanelState.Expanded) playAppearSample(); } StateChanged?.Invoke(value); } } protected override void Update() { base.Update(); audioContent.Balance.Value = (ScreenSpaceDrawQuad.Centre.X / game.ScreenSpaceDrawQuad.Width) * 2 - 1; } private void playAppearSample() { var channel = samplePanelFocus?.GetChannel(); if (channel == null) return; channel.Frequency.Value = 0.99 + RNG.NextDouble(0.2); channel.Play(); } private void updateState() { topLayerContent?.FadeOut(content_fade_duration).Expire(); middleLayerContent?.FadeOut(content_fade_duration).Expire(); switch (state) { case PanelState.Expanded: Size = new Vector2(EXPANDED_WIDTH, expanded_height); topLayerBackground.FadeColour(expanded_top_layer_colour, RESIZE_DURATION, Easing.OutQuint); middleLayerBackground.FadeColour(expanded_middle_layer_colour, RESIZE_DURATION, Easing.OutQuint); bool firstLoad = topLayerContent == null; topLayerContentContainer.Add(topLayerContent = new ExpandedPanelTopContent(Score.User, firstLoad) { Alpha = 0 }); middleLayerContentContainer.Add(middleLayerContent = new ExpandedPanelMiddleContent(Score, displayWithFlair) { Alpha = 0 }); // only the first expanded display should happen with flair. displayWithFlair = false; break; case PanelState.Contracted: Size = new Vector2(CONTRACTED_WIDTH, contracted_height); topLayerBackground.FadeColour(contracted_top_layer_colour, RESIZE_DURATION, Easing.OutQuint); middleLayerBackground.FadeColour(contracted_middle_layer_colour, RESIZE_DURATION, Easing.OutQuint); topLayerContentContainer.Add(topLayerContent = new ContractedPanelTopContent { ScorePosition = { BindTarget = ScorePosition }, Alpha = 0 }); middleLayerContentContainer.Add(middleLayerContent = new ContractedPanelMiddleContent(Score) { Alpha = 0 }); break; } audioContent.ResizeTo(Size, RESIZE_DURATION, Easing.OutQuint); bool topLayerExpanded = topLayerContainer.Y < 0; // If the top layer was already expanded, then we don't need to wait for the resize and can instead transform immediately. This looks better when changing the panel state. using (BeginDelayedSequence(topLayerExpanded ? 0 : RESIZE_DURATION + TOP_LAYER_EXPAND_DELAY)) { topLayerContainer.FadeIn(); switch (state) { case PanelState.Expanded: topLayerContainer.MoveToY(-expanded_top_layer_height / 2, top_layer_expand_duration, Easing.OutQuint); middleLayerContainer.MoveToY(expanded_top_layer_height / 2, top_layer_expand_duration, Easing.OutQuint); break; case PanelState.Contracted: topLayerContainer.MoveToY(-contracted_top_layer_height / 2, top_layer_expand_duration, Easing.OutQuint); middleLayerContainer.MoveToY(contracted_top_layer_height / 2, top_layer_expand_duration, Easing.OutQuint); break; } topLayerContent?.FadeIn(content_fade_duration); middleLayerContent?.FadeIn(content_fade_duration); } } public override Vector2 Size { get => base.Size; set { base.Size = value; // Auto-size isn't used to avoid 1-frame issues and because the score panel is removed/re-added to the container. if (trackingContainer != null) trackingContainer.Size = value; } } protected override bool OnClick(ClickEvent e) { if (State == PanelState.Contracted) { State = PanelState.Expanded; return true; } PostExpandAction?.Invoke(); return true; } public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => base.ReceivePositionalInputAt(screenSpacePos) || topLayerContainer.ReceivePositionalInputAt(screenSpacePos) || middleLayerContainer.ReceivePositionalInputAt(screenSpacePos); private ScorePanelTrackingContainer trackingContainer; /// <summary> /// Creates a <see cref="ScorePanelTrackingContainer"/> which this <see cref="ScorePanel"/> can reside inside. /// The <see cref="ScorePanelTrackingContainer"/> will track the size of this <see cref="ScorePanel"/>. /// </summary> /// <remarks> /// This <see cref="ScorePanel"/> is immediately added as a child of the <see cref="ScorePanelTrackingContainer"/>. /// </remarks> /// <returns>The <see cref="ScorePanelTrackingContainer"/>.</returns> /// <exception cref="InvalidOperationException">If a <see cref="ScorePanelTrackingContainer"/> already exists.</exception> public ScorePanelTrackingContainer CreateTrackingContainer() { if (trackingContainer != null) throw new InvalidOperationException("A score panel container has already been created."); return trackingContainer = new ScorePanelTrackingContainer(this); } } }