From 856f907c864ea2e728c665479db512c89018624a Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Fri, 18 Apr 2025 05:27:16 -0400 Subject: [PATCH 1/2] Add beatmap metadata wedge --- .../TestSceneBeatmapMetadataWedge.cs | 165 +++++++++ .../Screens/SelectV2/BeatmapMetadataWedge.cs | 337 ++++++++++++++++++ .../BeatmapMetadataWedge_FailRetryDisplay.cs | 195 ++++++++++ .../BeatmapMetadataWedge_MetadataDisplay.cs | 174 +++++++++ ...eatmapMetadataWedge_RatingSpreadDisplay.cs | 123 +++++++ ...BeatmapMetadataWedge_SuccessRateDisplay.cs | 112 ++++++ .../SelectV2/BeatmapMetadataWedge_TagsLine.cs | 223 ++++++++++++ .../BeatmapMetadataWedge_UserRatingDisplay.cs | 130 +++++++ 8 files changed, 1459 insertions(+) create mode 100644 osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapMetadataWedge.cs create mode 100644 osu.Game/Screens/SelectV2/BeatmapMetadataWedge.cs create mode 100644 osu.Game/Screens/SelectV2/BeatmapMetadataWedge_FailRetryDisplay.cs create mode 100644 osu.Game/Screens/SelectV2/BeatmapMetadataWedge_MetadataDisplay.cs create mode 100644 osu.Game/Screens/SelectV2/BeatmapMetadataWedge_RatingSpreadDisplay.cs create mode 100644 osu.Game/Screens/SelectV2/BeatmapMetadataWedge_SuccessRateDisplay.cs create mode 100644 osu.Game/Screens/SelectV2/BeatmapMetadataWedge_TagsLine.cs create mode 100644 osu.Game/Screens/SelectV2/BeatmapMetadataWedge_UserRatingDisplay.cs diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapMetadataWedge.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapMetadataWedge.cs new file mode 100644 index 000000000000..769188eb7109 --- /dev/null +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapMetadataWedge.cs @@ -0,0 +1,165 @@ +// 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.Linq; +using NUnit.Framework; +using osu.Framework.Graphics.Containers; +using osu.Game.Beatmaps; +using osu.Game.Models; +using osu.Game.Online.API; +using osu.Game.Online.API.Requests; +using osu.Game.Online.API.Requests.Responses; +using osu.Game.Screens.SelectV2; + +namespace osu.Game.Tests.Visual.SongSelectV2 +{ + public partial class TestSceneBeatmapMetadataWedge : SongSelectComponentsTestScene + { + private APIBeatmapSet? currentOnlineSet; + + protected override void LoadComplete() + { + base.LoadComplete(); + + ((DummyAPIAccess)API).HandleRequest = request => + { + switch (request) + { + case GetBeatmapSetRequest set: + if (set.ID == currentOnlineSet?.OnlineID) + { + set.TriggerSuccess(currentOnlineSet); + return true; + } + + return false; + + default: + return false; + } + }; + + Child = new BeatmapMetadataWedge + { + State = { Value = Visibility.Visible }, + }; + } + + [Test] + public void TestDisplay() + { + AddStep("null beatmap", () => Beatmap.SetDefault()); + AddStep("all metrics", () => + { + var (working, onlineSet) = createTestBeatmap(); + currentOnlineSet = onlineSet; + Beatmap.Value = working; + }); + AddStep("no source", () => + { + var (working, onlineSet) = createTestBeatmap(); + + working.Metadata.Source = string.Empty; + + currentOnlineSet = onlineSet; + Beatmap.Value = working; + }); + AddStep("no success rate", () => + { + var (working, onlineSet) = createTestBeatmap(); + + onlineSet.Beatmaps.Single().PlayCount = 0; + onlineSet.Beatmaps.Single().PassCount = 0; + + currentOnlineSet = onlineSet; + Beatmap.Value = working; + }); + AddStep("no user ratings", () => + { + var (working, onlineSet) = createTestBeatmap(); + + onlineSet.Ratings = Array.Empty(); + + currentOnlineSet = onlineSet; + Beatmap.Value = working; + }); + AddStep("no fail times", () => + { + var (working, onlineSet) = createTestBeatmap(); + + onlineSet.Beatmaps.Single().FailTimes = null; + + currentOnlineSet = onlineSet; + Beatmap.Value = working; + }); + AddStep("no metrics", () => + { + var (working, onlineSet) = createTestBeatmap(); + + onlineSet.Ratings = Array.Empty(); + onlineSet.Beatmaps.Single().FailTimes = null; + + currentOnlineSet = onlineSet; + Beatmap.Value = working; + }); + AddStep("local beatmap", () => + { + var (working, onlineSet) = createTestBeatmap(); + + working.BeatmapInfo.OnlineID = 0; + + currentOnlineSet = onlineSet; + Beatmap.Value = working; + }); + } + + [Test] + public void TestTruncation() + { + AddStep("long text", () => + { + var (working, onlineSet) = createTestBeatmap(); + + working.BeatmapInfo.Metadata.Author = new RealmUser { Username = "Verrrrryyyy llooonngggggg author" }; + working.BeatmapInfo.Metadata.Source = "Verrrrryyyy llooonngggggg source"; + working.BeatmapInfo.Metadata.Tags = string.Join(' ', Enumerable.Repeat(working.BeatmapInfo.Metadata.Tags, 3)); + onlineSet.Genre = new BeatmapSetOnlineGenre { Id = 12, Name = "Verrrrryyyy llooonngggggg genre" }; + onlineSet.Language = new BeatmapSetOnlineLanguage { Id = 12, Name = "Verrrrryyyy llooonngggggg language" }; + + currentOnlineSet = onlineSet; + Beatmap.Value = working; + }); + } + + private (WorkingBeatmap, APIBeatmapSet) createTestBeatmap() + { + var working = CreateWorkingBeatmap(Ruleset.Value); + var onlineSet = new APIBeatmapSet + { + OnlineID = working.BeatmapSetInfo.OnlineID, + Genre = new BeatmapSetOnlineGenre { Id = 15, Name = "Pop" }, + Language = new BeatmapSetOnlineLanguage { Id = 15, Name = "English" }, + Ratings = Enumerable.Range(0, 11).ToArray(), + Beatmaps = new[] + { + new APIBeatmap + { + OnlineID = working.BeatmapInfo.OnlineID, + PlayCount = 10000, + PassCount = 4567, + FailTimes = new APIFailTimes + { + Fails = Enumerable.Range(1, 100).Select(i => i % 12 - 6).ToArray(), + Retries = Enumerable.Range(-2, 100).Select(i => i % 12 - 6).ToArray(), + }, + }, + } + }; + + working.BeatmapSetInfo.DateSubmitted = DateTimeOffset.Now; + working.BeatmapSetInfo.DateRanked = DateTimeOffset.Now; + return (working, onlineSet); + } + } +} diff --git a/osu.Game/Screens/SelectV2/BeatmapMetadataWedge.cs b/osu.Game/Screens/SelectV2/BeatmapMetadataWedge.cs new file mode 100644 index 000000000000..a83ec51b11b0 --- /dev/null +++ b/osu.Game/Screens/SelectV2/BeatmapMetadataWedge.cs @@ -0,0 +1,337 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Linq; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Game.Beatmaps; +using osu.Game.Graphics.Containers; +using osu.Game.Online; +using osu.Game.Online.API; +using osu.Game.Online.API.Requests; +using osu.Game.Online.API.Requests.Responses; +using osu.Game.Online.Chat; +using osuTK; + +namespace osu.Game.Screens.SelectV2 +{ + public partial class BeatmapMetadataWedge : VisibilityContainer + { + private MetadataDisplay creator = null!; + private MetadataDisplay source = null!; + private MetadataDisplay genre = null!; + private MetadataDisplay language = null!; + private MetadataDisplay tag = null!; + private MetadataDisplay submitted = null!; + private MetadataDisplay ranked = null!; + + private Drawable ratingsWedge = null!; + private SuccessRateDisplay successRateDisplay = null!; + private UserRatingDisplay userRatingDisplay = null!; + private RatingSpreadDisplay ratingSpreadDisplay = null!; + + private Drawable failRetryWedge = null!; + private FailRetryDisplay failRetryDisplay = null!; + + [Resolved] + private IBindable beatmap { get; set; } = null!; + + [Resolved] + private IAPIProvider api { get; set; } = null!; + + private IBindable apiState = null!; + + [Resolved] + private ILinkHandler? linkHandler { get; set; } + + [Resolved] + private SongSelect? songSelect { get; set; } + + [BackgroundDependencyLoader] + private void load() + { + RelativeSizeAxes = Axes.X; + AutoSizeAxes = Axes.Y; + Padding = new MarginPadding { Top = 4f }; + + Width = 0.9f; + + InternalChild = new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Vertical, + Spacing = new Vector2(0f, 4f), + Shear = OsuGame.SHEAR, + Children = new[] + { + new ShearAligningWrapper(new Container + { + CornerRadius = 10, + Masking = true, + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Children = new Drawable[] + { + new WedgeBackground(), + new Container + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Shear = -OsuGame.SHEAR, + Padding = new MarginPadding { Left = SongSelect.WEDGE_CONTENT_MARGIN, Right = 35, Vertical = 16 }, + Children = new Drawable[] + { + new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Vertical, + Spacing = new Vector2(0f, 10f), + Children = new Drawable[] + { + new GridContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + RowDimensions = new[] { new Dimension(GridSizeMode.AutoSize) }, + ColumnDimensions = new[] + { + new Dimension(), + new Dimension(), + new Dimension(), + }, + Content = new[] + { + new[] + { + new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Vertical, + Spacing = new Vector2(0f, 10f), + Children = new[] + { + creator = new MetadataDisplay("Creator"), + genre = new MetadataDisplay("Genre"), + }, + }, + new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Vertical, + Spacing = new Vector2(0f, 10f), + Children = new[] + { + source = new MetadataDisplay("Source"), + language = new MetadataDisplay("Language"), + }, + }, + new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Vertical, + Spacing = new Vector2(0f, 10f), + Children = new[] + { + submitted = new MetadataDisplay("Submitted"), + ranked = new MetadataDisplay("Ranked"), + }, + }, + }, + }, + }, + tag = new MetadataDisplay("Tags"), + }, + }, + }, + }, + }, + }), + new ShearAligningWrapper(ratingsWedge = new Container + { + Alpha = 0f, + CornerRadius = 10, + Masking = true, + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Children = new Drawable[] + { + new WedgeBackground(), + new GridContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Shear = -OsuGame.SHEAR, + RowDimensions = new[] { new Dimension(GridSizeMode.AutoSize) }, + ColumnDimensions = new[] + { + new Dimension(), + new Dimension(GridSizeMode.Absolute, 10), + new Dimension(), + new Dimension(GridSizeMode.Absolute, 10), + new Dimension(), + }, + Padding = new MarginPadding { Left = SongSelect.WEDGE_CONTENT_MARGIN, Right = 40f, Vertical = 16 }, + Content = new[] + { + new[] + { + successRateDisplay = new SuccessRateDisplay(), + Empty(), + userRatingDisplay = new UserRatingDisplay(), + Empty(), + ratingSpreadDisplay = new RatingSpreadDisplay(), + }, + }, + }, + } + }), + new ShearAligningWrapper(failRetryWedge = new Container + { + Alpha = 0f, + CornerRadius = 10, + Masking = true, + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Children = new Drawable[] + { + new WedgeBackground(), + new Container + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Shear = -OsuGame.SHEAR, + Padding = new MarginPadding { Left = SongSelect.WEDGE_CONTENT_MARGIN, Right = 40f, Vertical = 16 }, + Child = failRetryDisplay = new FailRetryDisplay(), + }, + }, + }), + } + }; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + beatmap.BindValueChanged(_ => updateDisplay()); + + apiState = api.State.GetBoundCopy(); + apiState.BindValueChanged(_ => Scheduler.AddOnce(updateDisplay), true); + } + + protected override void PopIn() + { + this.FadeIn(300, Easing.OutQuint) + .MoveToX(0, 300, Easing.OutQuint); + } + + protected override void PopOut() + { + this.FadeOut(300, Easing.OutQuint) + .MoveToX(-100, 300, Easing.OutQuint); + } + + private void updateDisplay() + { + var metadata = beatmap.Value.Metadata; + var beatmapSetInfo = beatmap.Value.BeatmapSetInfo; + + creator.Data = (metadata.Author.Username, () => linkHandler?.HandleLink(new LinkDetails(LinkAction.OpenUserProfile, metadata.Author))); + + if (!string.IsNullOrEmpty(metadata.Source)) + source.Data = (metadata.Source, () => songSelect?.Search(metadata.Source)); + else + source.Data = ("-", null); + + tag.Tags = (metadata.Tags.Split(' '), t => songSelect?.Search(t)); + submitted.Date = beatmapSetInfo.DateSubmitted; + ranked.Date = beatmapSetInfo.DateRanked; + + if (currentOnlineBeatmapSet == null || currentOnlineBeatmapSet.OnlineID != beatmapSetInfo.OnlineID) + refetchBeatmapSet(); + + updateOnlineDisplay(); + } + + private APIBeatmapSet? currentOnlineBeatmapSet; + private GetBeatmapSetRequest? currentRequest; + + private void refetchBeatmapSet() + { + var beatmapSetInfo = beatmap.Value.BeatmapSetInfo; + + currentRequest?.Cancel(); + currentRequest = null; + currentOnlineBeatmapSet = null; + + if (beatmapSetInfo.OnlineID >= 1) + { + // todo: consider introducing a BeatmapSetLookupCache for caching benefits. + currentRequest = new GetBeatmapSetRequest(beatmapSetInfo.OnlineID); + currentRequest.Failure += _ => updateOnlineDisplay(); + currentRequest.Success += s => + { + currentOnlineBeatmapSet = s; + updateOnlineDisplay(); + }; + + api.Queue(currentRequest); + } + } + + private void updateOnlineDisplay() + { + if (currentRequest?.CompletionState == APIRequestCompletionState.Waiting) + { + genre.Data = null; + language.Data = null; + } + else if (currentOnlineBeatmapSet == null) + { + genre.Data = ("-", null); + language.Data = ("-", null); + + ratingsWedge.FadeOut(300, Easing.OutQuint); + ratingsWedge.MoveToX(-50, 300, Easing.OutQuint); + failRetryWedge.FadeOut(300, Easing.OutQuint); + failRetryWedge.MoveToX(-50, 300, Easing.OutQuint); + } + else + { + var beatmapInfo = beatmap.Value.BeatmapInfo; + + var onlineBeatmapSet = currentOnlineBeatmapSet; + var onlineBeatmap = onlineBeatmapSet.Beatmaps.SingleOrDefault(b => b.OnlineID == beatmapInfo.OnlineID); + + genre.Data = (onlineBeatmapSet.Genre.Name, () => songSelect?.Search(onlineBeatmapSet.Genre.Name)); + language.Data = (onlineBeatmapSet.Language.Name, () => songSelect?.Search(onlineBeatmapSet.Language.Name)); + + if (onlineBeatmap != null) + { + ratingsWedge.FadeIn(300, Easing.OutQuint); + ratingsWedge.MoveToX(0, 300, Easing.OutQuint); + failRetryWedge.FadeIn(300, Easing.OutQuint); + failRetryWedge.MoveToX(0, 300, Easing.OutQuint); + + userRatingDisplay.Data = onlineBeatmapSet.Ratings; + ratingSpreadDisplay.Data = onlineBeatmapSet.Ratings; + successRateDisplay.Data = (onlineBeatmap.PassCount, onlineBeatmap.PlayCount); + failRetryDisplay.Data = onlineBeatmap.FailTimes ?? new APIFailTimes(); + } + else + { + ratingsWedge.FadeOut(300, Easing.OutQuint); + ratingsWedge.MoveToX(-50, 300, Easing.OutQuint); + failRetryWedge.FadeOut(300, Easing.OutQuint); + failRetryWedge.MoveToX(-50, 300, Easing.OutQuint); + } + } + } + } +} diff --git a/osu.Game/Screens/SelectV2/BeatmapMetadataWedge_FailRetryDisplay.cs b/osu.Game/Screens/SelectV2/BeatmapMetadataWedge_FailRetryDisplay.cs new file mode 100644 index 000000000000..048ec3c40d94 --- /dev/null +++ b/osu.Game/Screens/SelectV2/BeatmapMetadataWedge_FailRetryDisplay.cs @@ -0,0 +1,195 @@ +// 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.Linq; +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Primitives; +using osu.Framework.Graphics.Rendering; +using osu.Framework.Utils; +using osu.Game.Beatmaps; +using osu.Game.Graphics; +using osu.Game.Graphics.Sprites; +using osu.Game.Resources.Localisation.Web; +using osuTK; + +namespace osu.Game.Screens.SelectV2 +{ + public partial class BeatmapMetadataWedge + { + private partial class FailRetryDisplay : CompositeDrawable + { + private readonly GraphDrawable retriesGraph; + private readonly GraphDrawable failsGraph; + + public APIFailTimes Data + { + set + { + int[] retries = value.Retries ?? Array.Empty(); + int[] fails = value.Fails ?? Array.Empty(); + int[] total = retries.Zip(fails, (r, f) => r + f).ToArray(); + + int maximum = total.DefaultIfEmpty(0).Max(); + + retriesGraph.Data = total.Select(r => maximum == 0 ? 0 : (float)r / maximum).ToArray(); + failsGraph.Data = fails.Select(r => maximum == 0 ? 0 : (float)r / maximum).ToArray(); + } + } + + public FailRetryDisplay() + { + RelativeSizeAxes = Axes.X; + AutoSizeAxes = Axes.Y; + + InternalChild = new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Vertical, + Spacing = new Vector2(0f, 4f), + Children = new Drawable[] + { + new OsuSpriteText + { + Text = BeatmapsetsStrings.ShowInfoPointsOfFailure, + Font = OsuFont.Style.Caption1.With(weight: FontWeight.SemiBold), + Margin = new MarginPadding { Bottom = 4f }, + }, + new Container + { + RelativeSizeAxes = Axes.X, + Height = 65f, + Children = new[] + { + retriesGraph = new GraphDrawable { RelativeSizeAxes = Axes.Both, Y = -1f }, + failsGraph = new GraphDrawable { RelativeSizeAxes = Axes.Both }, + }, + }, + }, + }; + } + + [BackgroundDependencyLoader] + private void load(OsuColour colours) + { + retriesGraph.Colour = colours.Orange1; + failsGraph.Colour = colours.DarkOrange2; + } + + private partial class GraphDrawable : Drawable + { + private readonly float[] displayedData = new float[100]; + + private float[] data = new float[100]; + + public float[] Data + { + get => data; + set + { + data = value; + Invalidate(Invalidation.DrawNode); + } + } + + protected override void Update() + { + base.Update(); + + bool changed = false; + + for (int i = 0; i < displayedData.Length; i++) + { + float before = displayedData[i]; + float value = data.ElementAtOrDefault(i); + displayedData[i] = (float)Interpolation.DampContinuously(displayedData[i], value, 40, Time.Elapsed); + changed |= displayedData[i] != before; + } + + if (changed) + Invalidate(Invalidation.DrawNode); + } + + protected override DrawNode CreateDrawNode() => new GraphDrawNode(this); + + // todo: consider integrating this with BarGraph + // this is different from BarGraph since this displays each bar with corner radii applied. + private class GraphDrawNode : DrawNode + { + private readonly GraphDrawable source; + + private Vector2 drawSize; + private float[] displayedData = null!; + + public GraphDrawNode(GraphDrawable source) + : base(source) + { + this.source = source; + } + + public override void ApplyState() + { + base.ApplyState(); + + drawSize = source.DrawSize; + displayedData = source.displayedData; + } + + protected override void Draw(IRenderer renderer) + { + base.Draw(renderer); + + const float spacing_constant = 1.5f; + + float position = 0; + float barWidth = drawSize.X / displayedData.Length / spacing_constant; + + float totalSpacing = drawSize.X - barWidth * displayedData.Length; + float spacing = totalSpacing / (displayedData.Length - 1); + + for (int i = 0; i < displayedData.Length; i++) + { + float barHeight = MathF.Max(drawSize.Y * displayedData[i], barWidth); + + drawBar(renderer, position, barWidth, barHeight); + + position += barWidth + spacing; + } + } + + private void drawBar(IRenderer renderer, float position, float width, float height) + { + float cornerRadius = width / 2f; + + Vector3 scale = DrawInfo.MatrixInverse.ExtractScale(); + float blendRange = (scale.X + scale.Y) / 2; + + RectangleF drawRectangle = new RectangleF(new Vector2(position, drawSize.Y - height), new Vector2(width, height)); + Quad screenSpaceDrawQuad = Quad.FromRectangle(drawRectangle) * DrawInfo.Matrix; + + renderer.PushMaskingInfo(new MaskingInfo + { + ScreenSpaceAABB = screenSpaceDrawQuad.AABB, + MaskingRect = drawRectangle.Normalize(), + ConservativeScreenSpaceQuad = screenSpaceDrawQuad, + ToMaskingSpace = DrawInfo.MatrixInverse, + CornerRadius = cornerRadius, + CornerExponent = 2f, + // We are setting the linear blend range to the approximate size of a _pixel_ here. + // This results in the optimal trade-off between crispness and smoothness of the + // edges of the masked region according to sampling theory. + BlendRange = blendRange, + AlphaExponent = 1, + }); + + renderer.DrawQuad(renderer.WhitePixel, screenSpaceDrawQuad, DrawColourInfo.Colour); + renderer.PopMaskingInfo(); + } + } + } + } + } +} diff --git a/osu.Game/Screens/SelectV2/BeatmapMetadataWedge_MetadataDisplay.cs b/osu.Game/Screens/SelectV2/BeatmapMetadataWedge_MetadataDisplay.cs new file mode 100644 index 000000000000..897349b9cb6b --- /dev/null +++ b/osu.Game/Screens/SelectV2/BeatmapMetadataWedge_MetadataDisplay.cs @@ -0,0 +1,174 @@ +// 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.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Localisation; +using osu.Game.Graphics; +using osu.Game.Graphics.Containers; +using osu.Game.Graphics.Sprites; +using osu.Game.Graphics.UserInterface; +using osu.Game.Overlays; +using osuTK; + +namespace osu.Game.Screens.SelectV2 +{ + public partial class BeatmapMetadataWedge + { + private partial class MetadataDisplay : FillFlowContainer + { + private readonly OsuSpriteText labelText; + private readonly OsuSpriteText contentText; + private readonly OsuSpriteText contentLinkText; + private readonly OsuHoverContainer contentLink; + private readonly DrawableDate contentDate; + private readonly TagsLine contentTags; + private readonly LoadingSpinner contentLoading; + + private (LocalisableString value, Action? linkAction)? data; + + public (LocalisableString value, Action? linkAction)? Data + { + get => data; + set + { + data = value; + + if (value?.linkAction != null) + setLink(value.Value.value, value.Value.linkAction); + else if (value.HasValue) + setText(value.Value.value); + else + setLoading(); + } + } + + public DateTimeOffset? Date + { + set + { + if (value != null) + setDate(value.Value); + else + setText("-"); + } + } + + public (string[] tags, Action linkAction) Tags + { + set => setTags(value.tags, value.linkAction); + } + + public MetadataDisplay(LocalisableString label) + { + RelativeSizeAxes = Axes.X; + AutoSizeAxes = Axes.Y; + + Padding = new MarginPadding { Right = 10 }; + + InternalChildren = new Drawable[] + { + labelText = new OsuSpriteText + { + Text = label, + Font = OsuFont.Style.Caption1.With(weight: FontWeight.SemiBold), + }, + new Container + { + RelativeSizeAxes = Axes.X, + Height = OsuFont.Style.Caption1.Size, + Children = new Drawable[] + { + contentText = new TruncatingSpriteText + { + RelativeSizeAxes = Axes.X, + Font = OsuFont.Style.Caption1, + }, + contentLink = new OsuHoverContainer + { + AutoSizeAxes = Axes.Both, + Child = contentLinkText = new TruncatingSpriteText + { + Font = OsuFont.Style.Caption1, + }, + }, + contentDate = new DrawableDate(default, OsuFont.Style.Caption1.Size, false), + contentTags = new TagsLine(), + contentLoading = new LoadingSpinner + { + Anchor = Anchor.TopLeft, + Origin = Anchor.TopLeft, + Size = new Vector2(10), + Margin = new MarginPadding { Top = 3f }, + State = { Value = Visibility.Visible }, + } + }, + }, + }; + } + + [BackgroundDependencyLoader] + private void load(OverlayColourProvider colourProvider) + { + labelText.Colour = colourProvider.Content1; + contentText.Colour = colourProvider.Content2; + contentLink.IdleColour = colourProvider.Light2; + } + + protected override void Update() + { + base.Update(); + contentLinkText.MaxWidth = ChildSize.X; + } + + private void clear() + { + contentText.Text = string.Empty; + contentLinkText.Text = string.Empty; + contentDate.Hide(); + contentTags.Tags = Array.Empty(); + contentLoading.Hide(); + } + + private void setText(LocalisableString text) + { + clear(); + + contentText.Text = text; + } + + private void setLink(LocalisableString text, Action action) => Schedule(() => + { + clear(); + + contentLinkText.Text = text; + contentLink.Action = action; + }); + + private void setDate(DateTimeOffset date) + { + clear(); + + contentDate.Show(); + contentDate.Date = date; + } + + private void setTags(string[] tags, Action link) + { + clear(); + + contentTags.Tags = tags; + contentTags.Action = link; + } + + private void setLoading() + { + clear(); + + contentLoading.Show(); + } + } + } +} diff --git a/osu.Game/Screens/SelectV2/BeatmapMetadataWedge_RatingSpreadDisplay.cs b/osu.Game/Screens/SelectV2/BeatmapMetadataWedge_RatingSpreadDisplay.cs new file mode 100644 index 000000000000..ee938ecdd9d9 --- /dev/null +++ b/osu.Game/Screens/SelectV2/BeatmapMetadataWedge_RatingSpreadDisplay.cs @@ -0,0 +1,123 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Linq; +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Colour; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Utils; +using osu.Game.Graphics; +using osu.Game.Graphics.Sprites; +using osu.Game.Resources.Localisation.Web; +using osuTK; + +namespace osu.Game.Screens.SelectV2 +{ + public partial class BeatmapMetadataWedge + { + private partial class RatingSpreadDisplay : CompositeDrawable + { + private const float min_height = 4f; + private const float max_height = 32f; + + private const int rating_range = 10; + + private readonly GraphBar[] graph; + + public int[] Data + { + set + { + if (!value.Any()) + { + foreach (var bar in graph) + bar.ResizeHeightTo(min_height, 300, Easing.OutQuint); + } + else + { + var usableRange = value.Skip(1).Take(rating_range); // adjust for API returning weird empty data at 0. + int maxRating = usableRange.Max(); + + for (int i = 0; i < graph.Length; i++) + graph[i].ResizeHeightTo(min_height + (max_height - min_height) * (maxRating == 0 ? 0 : usableRange.ElementAt(i) / (float)maxRating), 300, Easing.OutQuint); + } + } + } + + public RatingSpreadDisplay() + { + RelativeSizeAxes = Axes.X; + AutoSizeAxes = Axes.Y; + + graph = Enumerable.Range(0, rating_range).Select(_ => new GraphBar()).ToArray(); + + InternalChildren = new[] + { + new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Vertical, + Spacing = new Vector2(0f, 1f), + Children = new Drawable[] + { + new OsuSpriteText + { + Text = BeatmapsetsStrings.ShowStatsRatingSpread, + Font = OsuFont.Style.Caption1.With(weight: FontWeight.SemiBold), + }, + new GridContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + RowDimensions = new[] { new Dimension(GridSizeMode.Absolute, max_height) }, + ColumnDimensions = graph.SkipLast(1).Select(_ => new[] + { + new Dimension(), + new Dimension(GridSizeMode.Absolute, 1f), + }).SelectMany(d => d).Append(new Dimension()).ToArray(), + Content = new[] + { + graph.SkipLast(1).Select(g => new[] + { + g, + Empty() + }).SelectMany(g => g).Append(graph[^1]).ToArray() + }, + } + }, + } + }; + } + + [BackgroundDependencyLoader] + private void load(OsuColour colours) + { + for (int i = 0; i < 10; i++) + { + var left = Interpolation.ValueAt(i, colours.Blue4, colours.Blue0, 0, 10); + var right = Interpolation.ValueAt(i + 1, colours.Blue4, colours.Blue0, 0, 10); + graph[i].Colour = ColourInfo.GradientHorizontal(left, right); + } + } + + private partial class GraphBar : CompositeDrawable + { + [BackgroundDependencyLoader] + private void load() + { + Anchor = Anchor.BottomLeft; + Origin = Anchor.BottomLeft; + + RelativeSizeAxes = Axes.X; + CornerRadius = 2f; + Masking = true; + + InternalChild = new Box { RelativeSizeAxes = Axes.Both }; + } + } + } + } +} diff --git a/osu.Game/Screens/SelectV2/BeatmapMetadataWedge_SuccessRateDisplay.cs b/osu.Game/Screens/SelectV2/BeatmapMetadataWedge_SuccessRateDisplay.cs new file mode 100644 index 000000000000..611854727466 --- /dev/null +++ b/osu.Game/Screens/SelectV2/BeatmapMetadataWedge_SuccessRateDisplay.cs @@ -0,0 +1,112 @@ +// 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.Allocation; +using osu.Framework.Extensions.LocalisationExtensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Cursor; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Localisation; +using osu.Game.Graphics; +using osu.Game.Graphics.Sprites; +using osu.Game.Overlays; +using osu.Game.Resources.Localisation.Web; +using osuTK; + +namespace osu.Game.Screens.SelectV2 +{ + public partial class BeatmapMetadataWedge + { + private partial class SuccessRateDisplay : CompositeDrawable, IHasTooltip + { + private readonly OsuSpriteText valueText; + private readonly Circle backgroundBar; + private readonly Circle valueBar; + + private (int passes, int plays) data; + + public (int passes, int plays) Data + { + get => data; + set + { + data = value; + + float ratio = value.plays == 0 ? 0 : (float)value.passes / value.plays; + + valueText.Text = ratio.ToLocalisableString(@"0.##%"); + valueText.MoveToX(Math.Clamp(ratio, 0.05f, 0.95f), 300, Easing.OutQuint); + valueBar.ResizeWidthTo(ratio, 300, Easing.OutQuint); + } + } + + public LocalisableString TooltipText => $"{data.passes:N0} / {data.plays:N0}"; + + public SuccessRateDisplay() + { + RelativeSizeAxes = Axes.X; + AutoSizeAxes = Axes.Y; + + InternalChildren = new[] + { + new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Vertical, + Spacing = new Vector2(0f, 2f), + Children = new Drawable[] + { + new OsuSpriteText + { + Text = BeatmapsetsStrings.ShowInfoSuccessRate, + Font = OsuFont.Style.Caption1.With(weight: FontWeight.SemiBold), + }, + new Container + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Margin = new MarginPadding { Top = 10f }, + Child = valueText = new OsuSpriteText + { + Origin = Anchor.TopCentre, + RelativePositionAxes = Axes.X, + Font = OsuFont.Style.Caption1, + } + }, + new Container + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Children = new[] + { + backgroundBar = new Circle + { + RelativeSizeAxes = Axes.X, + Height = 4f, + }, + valueBar = new Circle + { + RelativeSizeAxes = Axes.X, + Width = 0f, + Height = 4f, + }, + }, + } + }, + } + }; + } + + [BackgroundDependencyLoader] + private void load(OsuColour colours, OverlayColourProvider colourProvider) + { + backgroundBar.Colour = colourProvider.Background6; + valueBar.Colour = colours.Lime1; + valueText.Colour = colourProvider.Content2; + } + } + } +} diff --git a/osu.Game/Screens/SelectV2/BeatmapMetadataWedge_TagsLine.cs b/osu.Game/Screens/SelectV2/BeatmapMetadataWedge_TagsLine.cs new file mode 100644 index 000000000000..56b83a2578e2 --- /dev/null +++ b/osu.Game/Screens/SelectV2/BeatmapMetadataWedge_TagsLine.cs @@ -0,0 +1,223 @@ +// 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.Diagnostics; +using System.Linq; +using osu.Framework.Allocation; +using osu.Framework.Extensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Cursor; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Graphics.UserInterface; +using osu.Framework.Input.Events; +using osu.Framework.Layout; +using osu.Game.Graphics; +using osu.Game.Graphics.Containers; +using osu.Game.Graphics.Sprites; +using osu.Game.Graphics.UserInterfaceV2; +using osu.Game.Overlays; +using osuTK; + +namespace osu.Game.Screens.SelectV2 +{ + public partial class BeatmapMetadataWedge + { + private partial class TagsLine : FillFlowContainer + { + private readonly LayoutValue drawSizeLayout = new LayoutValue(Invalidation.DrawSize); + + private string[] tags = Array.Empty(); + + private TagsOverflowButton? overflowButton; + + public string[] Tags + { + get => tags; + set + { + tags = value; + updateTags(); + } + } + + public Action? Action; + + [Resolved] + private OverlayColourProvider colourProvider { get; set; } = null!; + + public TagsLine() + { + RelativeSizeAxes = Axes.X; + AutoSizeAxes = Axes.Y; + Direction = FillDirection.Horizontal; + Spacing = new Vector2(4, 0); + + AddLayout(drawSizeLayout); + } + + protected override void UpdateAfterChildren() + { + base.UpdateAfterChildren(); + + if (!drawSizeLayout.IsValid) + { + updateLayout(); + drawSizeLayout.Validate(); + } + } + + private void updateLayout() + { + if (tags.Length == 0) + return; + + Debug.Assert(overflowButton != null); + + float limit = DrawWidth - overflowButton.DrawWidth - 5; + bool showOverflow = false; + + foreach (var text in Children) + { + if (text.X + text.DrawWidth < limit) + text.Show(); + else + { + showOverflow = true; + text.AlwaysPresent = false; + text.Hide(); + } + } + + if (showOverflow) + overflowButton.Show(); + else + overflowButton.Hide(); + } + + private void updateTags() + { + ChildrenEnumerable = tags.Select(t => new OsuHoverContainer + { + AutoSizeAxes = Axes.Both, + Action = () => Action?.Invoke(t), + IdleColour = colourProvider.Light2, + AlwaysPresent = true, + Alpha = 0f, + Child = new OsuSpriteText + { + Text = t, + Font = OsuFont.Style.Caption1, + }, + }); + + Add(overflowButton = new TagsOverflowButton(tags) + { + Alpha = 0f, + }); + + drawSizeLayout.Invalidate(); + } + + private partial class TagsOverflowButton : CompositeDrawable, IHasPopover, IHasLineBaseHeight + { + private readonly string[] tags; + + private Box box = null!; + private OsuSpriteText text = null!; + + [Resolved] + private OverlayColourProvider colourProvider { get; set; } = null!; + + [Resolved] + private SongSelect? songSelect { get; set; } + + public float LineBaseHeight => text.LineBaseHeight; + + public TagsOverflowButton(string[] tags) + { + this.tags = tags; + } + + [BackgroundDependencyLoader] + private void load() + { + Size = new Vector2(OsuFont.Style.Caption1.Size); + CornerRadius = 1.5f; + Masking = true; + + InternalChildren = new Drawable[] + { + box = new Box + { + Colour = colourProvider.Light1, + RelativeSizeAxes = Axes.Both, + }, + text = new OsuSpriteText + { + Y = -2, + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + Text = "...", + Colour = colourProvider.Background4, + Font = OsuFont.Style.Caption1.With(weight: FontWeight.Bold), + } + }; + } + + protected override bool OnHover(HoverEvent e) + { + box.FadeColour(colourProvider.Content2, 300, Easing.OutQuint); + return base.OnHover(e); + } + + protected override void OnHoverLost(HoverLostEvent e) + { + box.FadeColour(colourProvider.Light1, 300, Easing.OutQuint); + base.OnHoverLost(e); + } + + protected override bool OnClick(ClickEvent e) + { + box.FlashColour(colourProvider.Content1, 300, Easing.OutQuint); + this.ShowPopover(); + return true; + } + + public Popover GetPopover() => new TagsOverflowPopover(tags, songSelect); + } + + public partial class TagsOverflowPopover : OsuPopover + { + private readonly string[] tags; + private readonly SongSelect? songSelect; + + public TagsOverflowPopover(string[] tags, SongSelect? songSelect) + { + this.tags = tags; + this.songSelect = songSelect; + } + + [BackgroundDependencyLoader] + private void load() + { + LinkFlowContainer textFlow; + + Child = textFlow = new LinkFlowContainer(t => t.Font = OsuFont.Style.Caption1) + { + Width = 200, + AutoSizeAxes = Axes.Y, + }; + + foreach (string tag in tags) + { + textFlow.AddLink(tag, () => songSelect?.Search(tag)); + textFlow.AddText(" "); + } + } + } + } + } +} diff --git a/osu.Game/Screens/SelectV2/BeatmapMetadataWedge_UserRatingDisplay.cs b/osu.Game/Screens/SelectV2/BeatmapMetadataWedge_UserRatingDisplay.cs new file mode 100644 index 000000000000..2f38079577d1 --- /dev/null +++ b/osu.Game/Screens/SelectV2/BeatmapMetadataWedge_UserRatingDisplay.cs @@ -0,0 +1,130 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Linq; +using osu.Framework.Allocation; +using osu.Framework.Extensions.LocalisationExtensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Game.Graphics; +using osu.Game.Graphics.Sprites; +using osu.Game.Overlays; +using osu.Game.Resources.Localisation.Web; +using osuTK; + +namespace osu.Game.Screens.SelectV2 +{ + public partial class BeatmapMetadataWedge + { + private partial class UserRatingDisplay : CompositeDrawable + { + private readonly OsuSpriteText negativeText; + private readonly OsuSpriteText positiveText; + private readonly Circle backgroundBar; + private readonly Circle positiveBar; + + public int[] Data + { + set + { + const int rating_range = 10; + + if (!value.Any()) + { + negativeText.Text = 0.ToLocalisableString(@"N0"); + positiveText.Text = 0.ToLocalisableString(@"N0"); + positiveBar.ResizeWidthTo(0, 300, Easing.OutQuint); + } + else + { + var usableRange = value.Skip(1).Take(rating_range); // adjust for API returning weird empty data at 0. + + int positiveCount = usableRange.Skip(rating_range / 2).Sum(); + int totalCount = usableRange.Sum(); + + negativeText.Text = (totalCount - positiveCount).ToLocalisableString(@"N0"); + positiveText.Text = positiveCount.ToLocalisableString(@"N0"); + positiveBar.ResizeWidthTo(totalCount == 0 ? 0 : (float)positiveCount / totalCount, 300, Easing.OutQuint); + } + } + } + + public UserRatingDisplay() + { + RelativeSizeAxes = Axes.X; + AutoSizeAxes = Axes.Y; + + InternalChildren = new[] + { + new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Vertical, + Spacing = new Vector2(0f, 2f), + Children = new Drawable[] + { + new OsuSpriteText + { + Text = BeatmapsetsStrings.ShowStatsUserRating, + Font = OsuFont.Style.Caption1.With(weight: FontWeight.SemiBold), + }, + new Container + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Margin = new MarginPadding { Top = 10f }, + Children = new[] + { + negativeText = new OsuSpriteText + { + Anchor = Anchor.TopLeft, + Origin = Anchor.TopLeft, + Font = OsuFont.Style.Caption1, + }, + positiveText = new OsuSpriteText + { + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight, + Font = OsuFont.Style.Caption1, + }, + }, + }, + new Container + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Children = new[] + { + backgroundBar = new Circle + { + RelativeSizeAxes = Axes.X, + Height = 4f, + }, + positiveBar = new Circle + { + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight, + RelativeSizeAxes = Axes.X, + Width = 0f, + Height = 4f, + }, + }, + } + }, + } + }; + } + + [BackgroundDependencyLoader] + private void load(OsuColour colours, OverlayColourProvider colourProvider) + { + backgroundBar.Colour = colours.DarkOrange2; + positiveBar.Colour = colours.Lime1; + negativeText.Colour = colourProvider.Content2; + positiveText.Colour = colourProvider.Content2; + } + } + } +} From 883df07ff6d5fd8880c4fea079f8939a6996c103 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 23 Apr 2025 16:23:37 +0900 Subject: [PATCH 2/2] Adjust tests and transitions --- .../TestSceneBeatmapMetadataWedge.cs | 22 ++++++- .../Screens/SelectV2/BeatmapMetadataWedge.cs | 64 ++++++++++++------- 2 files changed, 61 insertions(+), 25 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapMetadataWedge.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapMetadataWedge.cs index 769188eb7109..be2e6eb9bf06 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapMetadataWedge.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapMetadataWedge.cs @@ -18,6 +18,8 @@ public partial class TestSceneBeatmapMetadataWedge : SongSelectComponentsTestSce { private APIBeatmapSet? currentOnlineSet; + private BeatmapMetadataWedge wedge = null!; + protected override void LoadComplete() { base.LoadComplete(); @@ -40,22 +42,36 @@ protected override void LoadComplete() } }; - Child = new BeatmapMetadataWedge + Child = wedge = new BeatmapMetadataWedge { State = { Value = Visibility.Visible }, }; } [Test] - public void TestDisplay() + public void TestShowHide() + { + AddStep("all metrics", () => + { + var (working, onlineSet) = createTestBeatmap(); + currentOnlineSet = onlineSet; + Beatmap.Value = working; + }); + + AddStep("hide wedge", () => wedge.Hide()); + AddStep("show wedge", () => wedge.Show()); + } + + [Test] + public void TestVariousMetrics() { - AddStep("null beatmap", () => Beatmap.SetDefault()); AddStep("all metrics", () => { var (working, onlineSet) = createTestBeatmap(); currentOnlineSet = onlineSet; Beatmap.Value = working; }); + AddStep("null beatmap", () => Beatmap.SetDefault()); AddStep("no source", () => { var (working, onlineSet) = createTestBeatmap(); diff --git a/osu.Game/Screens/SelectV2/BeatmapMetadataWedge.cs b/osu.Game/Screens/SelectV2/BeatmapMetadataWedge.cs index a83ec51b11b0..816dfc3f95cd 100644 --- a/osu.Game/Screens/SelectV2/BeatmapMetadataWedge.cs +++ b/osu.Game/Screens/SelectV2/BeatmapMetadataWedge.cs @@ -35,6 +35,8 @@ public partial class BeatmapMetadataWedge : VisibilityContainer private Drawable failRetryWedge = null!; private FailRetryDisplay failRetryDisplay = null!; + protected override bool StartHidden => true; + [Resolved] private IBindable beatmap { get; set; } = null!; @@ -225,16 +227,47 @@ protected override void LoadComplete() apiState.BindValueChanged(_ => Scheduler.AddOnce(updateDisplay), true); } + private const double transition_duration = 300; + protected override void PopIn() { - this.FadeIn(300, Easing.OutQuint) - .MoveToX(0, 300, Easing.OutQuint); + this.FadeIn(transition_duration, Easing.OutQuint) + .MoveToX(0, transition_duration, Easing.OutQuint); + + updateSubWedgeVisibility(); } protected override void PopOut() { - this.FadeOut(300, Easing.OutQuint) - .MoveToX(-100, 300, Easing.OutQuint); + this.FadeOut(transition_duration, Easing.OutQuint) + .MoveToX(-100, transition_duration, Easing.OutQuint); + + updateSubWedgeVisibility(); + } + + private void updateSubWedgeVisibility() + { + // We could consider hiding individual wedges based on zero data in the future. + // Needs some experimentation on what looks good. + + if (State.Value == Visibility.Visible && currentOnlineBeatmapSet != null) + { + ratingsWedge.FadeIn(transition_duration, Easing.OutQuint) + .MoveToX(0, transition_duration, Easing.OutQuint); + + failRetryWedge.Delay(100) + .FadeIn(transition_duration, Easing.OutQuint) + .MoveToX(0, transition_duration, Easing.OutQuint); + } + else + { + ratingsWedge.FadeOut(transition_duration, Easing.OutQuint) + .MoveToX(-50, transition_duration, Easing.OutQuint); + + failRetryWedge.Delay(100) + .FadeOut(transition_duration, Easing.OutQuint) + .MoveToX(-50, transition_duration, Easing.OutQuint); + } } private void updateDisplay() @@ -291,16 +324,13 @@ private void updateOnlineDisplay() { genre.Data = null; language.Data = null; + return; } - else if (currentOnlineBeatmapSet == null) + + if (currentOnlineBeatmapSet == null) { genre.Data = ("-", null); language.Data = ("-", null); - - ratingsWedge.FadeOut(300, Easing.OutQuint); - ratingsWedge.MoveToX(-50, 300, Easing.OutQuint); - failRetryWedge.FadeOut(300, Easing.OutQuint); - failRetryWedge.MoveToX(-50, 300, Easing.OutQuint); } else { @@ -314,24 +344,14 @@ private void updateOnlineDisplay() if (onlineBeatmap != null) { - ratingsWedge.FadeIn(300, Easing.OutQuint); - ratingsWedge.MoveToX(0, 300, Easing.OutQuint); - failRetryWedge.FadeIn(300, Easing.OutQuint); - failRetryWedge.MoveToX(0, 300, Easing.OutQuint); - userRatingDisplay.Data = onlineBeatmapSet.Ratings; ratingSpreadDisplay.Data = onlineBeatmapSet.Ratings; successRateDisplay.Data = (onlineBeatmap.PassCount, onlineBeatmap.PlayCount); failRetryDisplay.Data = onlineBeatmap.FailTimes ?? new APIFailTimes(); } - else - { - ratingsWedge.FadeOut(300, Easing.OutQuint); - ratingsWedge.MoveToX(-50, 300, Easing.OutQuint); - failRetryWedge.FadeOut(300, Easing.OutQuint); - failRetryWedge.MoveToX(-50, 300, Easing.OutQuint); - } } + + updateSubWedgeVisibility(); } } }