// 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; 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 partial class ScorePanel : CompositeDrawable, IStateful { /// /// Width of the panel when contracted. /// public const float CONTRACTED_WIDTH = 130; /// /// Height of the panel when contracted. /// private const float contracted_height = 385; /// /// Width of the panel when expanded. /// public const float EXPANDED_WIDTH = 360; /// /// Height of the panel when expanded. /// private const float expanded_height = 586; /// /// Height of the top layer when the panel is expanded. /// private const float expanded_top_layer_height = 53; /// /// Height of the top layer when the panel is contracted. /// private const float contracted_top_layer_height = 30; /// /// Duration for the panel to resize into its expanded/contracted size. /// public const double RESIZE_DURATION = 200; /// /// Delay after before the top layer is expanded. /// public const double TOP_LAYER_EXPAND_DELAY = 100; /// /// Duration for the top layer expansion. /// private const double top_layer_expand_duration = 200; /// /// Duration for the panel contents to fade in. /// 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? StateChanged; /// /// The position of the score in the rankings. /// public readonly Bindable ScorePosition = new Bindable(); /// /// An action to be invoked if this is clicked while in an expanded state. /// public Action? PostExpandAction; public readonly ScoreInfo Score; [Resolved] private OsuGameBase game { get; set; } = null!; private AudioContainer audioContent = null!; private bool displayWithFlair; private Container topLayerContainer = null!; private Drawable topLayerBackground = null!; private Container topLayerContentContainer = null!; private Drawable? topLayerContent; private Container middleLayerContainer = null!; private Drawable middleLayerBackground = null!; private Container middleLayerContentContainer = null!; private Drawable? middleLayerContent; private ScorePanelTrackingContainer? trackingContainer; 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) * OsuGameBase.SFX_STEREO_STRENGTH; } 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); /// /// Creates a which this can reside inside. /// The will track the size of this . /// /// /// This is immediately added as a child of the . /// /// The . /// If a already exists. public ScorePanelTrackingContainer CreateTrackingContainer() { if (trackingContainer != null) throw new InvalidOperationException("A score panel container has already been created."); return trackingContainer = new ScorePanelTrackingContainer(this); } } }