From e172e7f80cdbeeab37eaf0d77384e2dfb478611e Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Tue, 22 Jul 2025 23:58:43 +0300 Subject: [PATCH] Implement new gameplay leaderboard design --- .../Play/HUD/DrawableGameplayLeaderboard.cs | 6 +- .../HUD/DrawableGameplayLeaderboardScore.cs | 486 +++++++++--------- 2 files changed, 233 insertions(+), 259 deletions(-) diff --git a/osu.Game/Screens/Play/HUD/DrawableGameplayLeaderboard.cs b/osu.Game/Screens/Play/HUD/DrawableGameplayLeaderboard.cs index 1dd22301c0..b6eb6e1e27 100644 --- a/osu.Game/Screens/Play/HUD/DrawableGameplayLeaderboard.cs +++ b/osu.Game/Screens/Play/HUD/DrawableGameplayLeaderboard.cs @@ -54,7 +54,9 @@ namespace osu.Game.Screens.Play.HUD /// public DrawableGameplayLeaderboard() { - Width = DrawableGameplayLeaderboardScore.EXTENDED_WIDTH + DrawableGameplayLeaderboardScore.SHEAR_WIDTH; + float xOffset = DrawableGameplayLeaderboardScore.SHEAR_WIDTH + DrawableGameplayLeaderboardScore.ELASTIC_WIDTH_LENIENCE; + + Width = DrawableGameplayLeaderboardScore.EXTENDED_WIDTH + xOffset; Height = 300; InternalChildren = new Drawable[] @@ -66,7 +68,7 @@ namespace osu.Game.Screens.Play.HUD Child = Flow = new FillFlowContainer { RelativeSizeAxes = Axes.X, - X = DrawableGameplayLeaderboardScore.SHEAR_WIDTH, + X = xOffset, AutoSizeAxes = Axes.Y, Direction = FillDirection.Vertical, Spacing = new Vector2(2.5f), diff --git a/osu.Game/Screens/Play/HUD/DrawableGameplayLeaderboardScore.cs b/osu.Game/Screens/Play/HUD/DrawableGameplayLeaderboardScore.cs index e4f2cc0d68..2c138f8de1 100644 --- a/osu.Game/Screens/Play/HUD/DrawableGameplayLeaderboardScore.cs +++ b/osu.Game/Screens/Play/HUD/DrawableGameplayLeaderboardScore.cs @@ -7,6 +7,7 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; +using osu.Framework.Graphics.Colour; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Game.Configuration; @@ -25,32 +26,29 @@ namespace osu.Game.Screens.Play.HUD { public partial class DrawableGameplayLeaderboardScore : CompositeDrawable { - public const float EXTENDED_WIDTH = regular_width + top_player_left_width_extension; + public const float EXTENDED_WIDTH = extended_left_panel_width + right_panel_width; - private const float regular_width = 235f; + private const float left_panel_extension_width = 20; - // a bit hand-wavy, but there's a lot of hard-coded paddings in each of the grid's internals. - private const float compact_width = 77.5f; + private const float regular_left_panel_width = avatar_size + avatar_size / 2; + private const float extended_left_panel_width = regular_left_panel_width + left_panel_extension_width; + private const float right_panel_width = 180; - private const float top_player_left_width_extension = 20f; + private const float avatar_size = PANEL_HEIGHT; - public const float PANEL_HEIGHT = 35f; + public const float PANEL_HEIGHT = 38f; - public const float SHEAR_WIDTH = PANEL_HEIGHT * panel_shear; + public static readonly float SHEAR_WIDTH = PANEL_HEIGHT * OsuGame.SHEAR.X; - private const float panel_shear = 0.15f; - - private const float rank_text_width = 35f; - - private const float avatar_size = 25f; + /// + /// Extra width lenience to account for the out-of-range values produced by elastic easing when the score panel becomes extended (due to earning first score position or is a tracked score). + /// + public const float ELASTIC_WIDTH_LENIENCE = 10f; private const double panel_transition_duration = 500; - private const double text_transition_duration = 200; - public Bindable Expanded = new Bindable(); - - private OsuSpriteText positionText = null!, scoreText = null!, accuracyText = null!, comboText = null!, usernameText = null!; + public Bindable Expanded { get; } = new BindableBool(); public BindableLong TotalScore { get; } = new BindableLong(); public BindableDouble Accuracy { get; } = new BindableDouble(1); @@ -66,9 +64,7 @@ namespace osu.Game.Screens.Play.HUD set => getDisplayScoreFunction = value; } - public Color4? BackgroundColour { get; set; } - - public Color4? TextColour { get; set; } + public Color4? BackgroundColour { get; } public IUser? User { get; } @@ -77,20 +73,31 @@ namespace osu.Game.Screens.Play.HUD /// public readonly bool Tracked; - private Container mainFillContainer = null!; - - private Box centralFill = null!; - - private Container backgroundPaddingAdjustContainer = null!; - - private GridContainer gridContainer = null!; - + private FillFlowContainer scorePanel = null!; + private Container leftLayer = null!; + private Box leftLayerGradient = null!; + private Container rightLayer = null!; + private Box rightLayerGradient = null!; private Container scoreComponents = null!; + private OsuSpriteText usernameText = null!; + private OsuSpriteText positionText = null!; + private OsuSpriteText accuracyText = null!; + private OsuSpriteText scoreText = null!; + private OsuSpriteText comboText = null!; private IBindable scoreDisplayMode = null!; private bool isFriend; + [Resolved] + private OsuConfigManager config { get; set; } = null!; + + [Resolved] + private IAPIProvider api { get; set; } = null!; + + [Resolved] + private OsuColour colours { get; set; } = null!; + /// /// Creates a new . /// @@ -107,324 +114,289 @@ namespace osu.Game.Screens.Play.HUD GetDisplayScore = score.GetDisplayScore; if (score.TeamColour != null) - { BackgroundColour = score.TeamColour.Value; - TextColour = Color4.White; - } AutoSizeAxes = Axes.X; Height = PANEL_HEIGHT; + + Shear = OsuGame.SHEAR; } [BackgroundDependencyLoader] - private void load(OsuColour colours, OsuConfigManager osuConfigManager, IAPIProvider api) + private void load() { - Container avatarContainer; + const float corner_radius = 10; - InternalChildren = new Drawable[] + Container avatarLayer; + + InternalChild = scorePanel = new FillFlowContainer { - new Container + CornerRadius = corner_radius, + BorderThickness = 2f, + Masking = true, + AutoSizeAxes = Axes.X, + RelativeSizeAxes = Axes.Y, + Children = new[] { - AutoSizeAxes = Axes.X, - RelativeSizeAxes = Axes.Y, - Margin = new MarginPadding { Left = top_player_left_width_extension }, - Children = new Drawable[] + leftLayer = new Container { - backgroundPaddingAdjustContainer = new Container + Width = regular_left_panel_width, + RelativeSizeAxes = Axes.Y, + Children = new Drawable[] { - RelativeSizeAxes = Axes.Both, - Children = new Drawable[] + leftLayerGradient = new Box { - mainFillContainer = new Container + RelativeSizeAxes = Axes.Both, + }, + new Container + { + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight, + Width = regular_left_panel_width, + Padding = new MarginPadding { Right = avatar_size / 2 - SHEAR_WIDTH / 2 }, + RelativeSizeAxes = Axes.Y, + Child = positionText = new OsuSpriteText { - Anchor = Anchor.TopRight, - Origin = Anchor.TopRight, - RelativeSizeAxes = Axes.Both, - Masking = true, - CornerRadius = 5f, - Shear = new Vector2(panel_shear, 0f), - Children = new Drawable[] - { - new Box - { - Alpha = 0.5f, - RelativeSizeAxes = Axes.Both, - }, - }, - }, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Font = OsuFont.Style.Caption1.With(weight: FontWeight.SemiBold), + Shear = -OsuGame.SHEAR, + } } }, - gridContainer = new GridContainer + }, + // this is placed here between the left and right layer for layout purposes, + // but it's proxied below to render in front of them. + avatarLayer = new Container + { + Size = new Vector2(avatar_size), + // precise padding so the avatar's top and bottom sides land as close to the panel borders as possible. + Padding = new MarginPadding(1.3f), + // negative left margin to place the avatar's center directly at the edge of the left layer. + Margin = new MarginPadding { Left = -avatar_size / 2 }, + Child = new Container { - RelativeSizeAxes = Axes.Y, - Width = compact_width, // will be updated by expanded state. - Anchor = Anchor.TopRight, - Origin = Anchor.TopRight, - ColumnDimensions = new[] + RelativeSizeAxes = Axes.Both, + CornerRadius = corner_radius, + Masking = true, + Child = new ScoreAvatar(User) { - new Dimension(GridSizeMode.Absolute, rank_text_width), - new Dimension(), - new Dimension(GridSizeMode.AutoSize), + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.Both, + Shear = -OsuGame.SHEAR, + // extra scaling to cover the entire sheared area. + Scale = new Vector2(1.1f), }, - Content = new[] + }, + }, + rightLayer = new Container + { + Width = right_panel_width, + RelativeSizeAxes = Axes.Y, + // negative left margin to make the X position of the right layer directly at the avatar center (rendered behind it). + Margin = new MarginPadding { Left = -avatar_size / 2 }, + Children = new Drawable[] + { + rightLayerGradient = new Box { - new Drawable[] + RelativeSizeAxes = Axes.Both, + }, + scoreComponents = new Container + { + Width = right_panel_width, + RelativeSizeAxes = Axes.Y, + Padding = new MarginPadding { Left = avatar_size / 2 + 4, Right = 20, Vertical = 5 }, + Shear = -OsuGame.SHEAR, + Children = new Drawable[] { - positionText = new OsuSpriteText + new GridContainer { - Padding = new MarginPadding { Right = SHEAR_WIDTH / 2 }, - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Colour = Color4.White, - Font = OsuFont.Torus.With(size: 14, weight: FontWeight.Bold), - Shadow = false, + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + ColumnDimensions = new[] + { + new Dimension(), + new Dimension(GridSizeMode.Absolute, 10), + new Dimension(GridSizeMode.AutoSize), + }, + RowDimensions = new[] + { + new Dimension(GridSizeMode.AutoSize), + }, + Content = new[] + { + new[] + { + usernameText = new TruncatingSpriteText + { + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + Text = User?.Username ?? string.Empty, + Font = OsuFont.Style.Caption1.With(weight: FontWeight.SemiBold), + RelativeSizeAxes = Axes.X, + }, + Empty(), + accuracyText = new OsuSpriteText + { + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + Font = OsuFont.Style.Caption2.With(weight: FontWeight.SemiBold), + }, + } + }, }, new Container { - Padding = new MarginPadding { Horizontal = SHEAR_WIDTH / 3 }, - RelativeSizeAxes = Axes.Both, - Children = new Drawable[] - { - new Container - { - Masking = true, - CornerRadius = 5f, - Shear = new Vector2(panel_shear, 0f), - RelativeSizeAxes = Axes.Both, - Children = new[] - { - centralFill = new Box - { - Alpha = 0.5f, - RelativeSizeAxes = Axes.Both, - Colour = Color4Extensions.FromHex("3399cc"), - }, - } - }, - new FillFlowContainer - { - Padding = new MarginPadding { Left = SHEAR_WIDTH }, - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - RelativeSizeAxes = Axes.Both, - Direction = FillDirection.Horizontal, - Spacing = new Vector2(4f, 0f), - Children = new Drawable[] - { - avatarContainer = new CircularContainer - { - Masking = true, - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - Size = new Vector2(avatar_size), - Children = new Drawable[] - { - new Box - { - Name = "Placeholder while avatar loads", - Alpha = 0.3f, - RelativeSizeAxes = Axes.Both, - Colour = colours.Gray4, - } - } - }, - usernameText = new TruncatingSpriteText - { - RelativeSizeAxes = Axes.X, - Width = 0.6f, - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - Colour = Color4.White, - Font = OsuFont.Torus.With(size: 14, weight: FontWeight.SemiBold), - Text = User?.Username ?? string.Empty, - Shadow = false, - } - } - }, - } - }, - scoreComponents = new Container - { - Padding = new MarginPadding { Top = 2f, Right = 17.5f, Bottom = 5f }, - AlwaysPresent = true, // required to smoothly animate autosize after hidden early. - Masking = true, - RelativeSizeAxes = Axes.Y, - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - Colour = Color4.White, - Children = new Drawable[] + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Children = new[] { scoreText = new OsuSpriteText - { - Spacing = new Vector2(-1f, 0f), - Font = OsuFont.Torus.With(size: 16, weight: FontWeight.SemiBold, fixedWidth: true), - Shadow = false, - }, - accuracyText = new OsuSpriteText { Anchor = Anchor.BottomLeft, Origin = Anchor.BottomLeft, - Font = OsuFont.Torus.With(size: 12, weight: FontWeight.SemiBold, fixedWidth: true), - Spacing = new Vector2(-1f, 0f), - Shadow = false, + Font = OsuFont.Style.Body.With(weight: FontWeight.Regular), }, comboText = new OsuSpriteText { Anchor = Anchor.BottomRight, Origin = Anchor.BottomRight, - Spacing = new Vector2(-1f, 0f), - Font = OsuFont.Torus.With(size: 12, weight: FontWeight.SemiBold, fixedWidth: true), - Shadow = false, + Font = OsuFont.Style.Caption2.With(weight: FontWeight.SemiBold), }, - }, - } - } + } + }, + }, } } - } - }, + }, + avatarLayer.CreateProxy(), + } }; - - LoadComponentAsync(new DrawableAvatar(User), avatarContainer.Add); - - scoreDisplayMode = osuConfigManager.GetBindable(OsuSetting.ScoreDisplayMode); - scoreDisplayMode.BindValueChanged(_ => updateScore()); - TotalScore.BindValueChanged(_ => updateScore(), true); - - Accuracy.BindValueChanged(v => - { - accuracyText.Text = v.NewValue.FormatAccuracy(); - updateDetailsWidth(); - }, true); - - Combo.BindValueChanged(v => - { - comboText.Text = $"{v.NewValue}x"; - updateDetailsWidth(); - }, true); - - HasQuit.BindValueChanged(_ => updateState()); - - isFriend = User != null && api.Friends.Any(u => User.OnlineID == u.TargetID); } protected override void LoadComplete() { base.LoadComplete(); - updateState(); - Expanded.BindValueChanged(changeExpandedState, true); - ScorePosition.BindValueChanged(_ => updateState(), true); + isFriend = User != null && api.Friends.Any(u => User.OnlineID == u.TargetID); + + scoreDisplayMode = config.GetBindable(OsuSetting.ScoreDisplayMode); + scoreDisplayMode.BindValueChanged(_ => updateScore()); + TotalScore.BindValueChanged(_ => updateScore(), true); + + Accuracy.BindValueChanged(v => accuracyText.Text = v.NewValue.FormatAccuracy(), true); + + Combo.BindValueChanged(v => comboText.Text = $@"{v.NewValue}x", true); + + Expanded.BindValueChanged(onExpanded, true); + + HasQuit.BindValueChanged(_ => updatePanelState()); + ScorePosition.BindValueChanged(_ => updatePanelState(), true); FinishTransforms(true); } private void updateScore() => scoreText.Text = (getDisplayScoreFunction?.Invoke(scoreDisplayMode.Value) ?? TotalScore.Value).ToString("N0"); - private void changeExpandedState(ValueChangedEvent expanded) + private void onExpanded(ValueChangedEvent expanded) { if (expanded.NewValue) { - gridContainer.ResizeWidthTo(regular_width, panel_transition_duration, Easing.OutQuint); - + rightLayer.ResizeWidthTo(right_panel_width, panel_transition_duration, Easing.OutQuint); scoreComponents.FadeIn(panel_transition_duration, Easing.OutQuint); - - usernameText.FadeIn(panel_transition_duration, Easing.OutQuint); } else { - gridContainer.ResizeWidthTo(compact_width, panel_transition_duration, Easing.OutQuint); - + rightLayer.ResizeWidthTo(avatar_size / 2, panel_transition_duration, Easing.OutQuint); scoreComponents.FadeOut(text_transition_duration, Easing.OutQuint); - - usernameText.FadeOut(text_transition_duration, Easing.OutQuint); } - - updateDetailsWidth(); } - private float? scoreComponentsTargetWidth; - - private void updateDetailsWidth() + private void updatePanelState() { - const float score_components_min_width = 88f; + positionText.Text = ScorePosition.Value.HasValue ? $"#{ScorePosition.Value.Value.FormatRank()}" : "-"; - float newWidth = Expanded.Value - ? Math.Max(score_components_min_width, comboText.DrawWidth + accuracyText.DrawWidth + 25) - : 0; - - if (scoreComponentsTargetWidth == newWidth) - return; - - scoreComponentsTargetWidth = newWidth; - scoreComponents.ResizeWidthTo(newWidth, panel_transition_duration, Easing.OutQuint); - } - - private void updateState() - { + Color4 usernameColour = Color4.White; bool widthExtension = false; if (HasQuit.Value) { - // we will probably want to display this in a better way once we have a design. - // and also show states other than quit. - panelColour = Color4.Gray; - textColour = Color4.White; - return; + setPanelColour(Color4.Gray); + usernameColour = colours.Red2; } - - positionText.Text = ScorePosition.Value.HasValue ? $"#{ScorePosition.Value.Value.FormatRank()}" : "-"; - - if (ScorePosition.Value == 1) + else if (ScorePosition.Value == 1) { widthExtension = true; - panelColour = BackgroundColour ?? Color4Extensions.FromHex("7fcc33"); - textColour = TextColour ?? Color4.White; + setPanelColour(BackgroundColour ?? colours.Lime2); } else if (Tracked) { widthExtension = true; - panelColour = BackgroundColour ?? Color4Extensions.FromHex("ffd966"); - textColour = TextColour ?? Color4Extensions.FromHex("2e576b"); + setPanelColourAsTracked(); } else if (isFriend) { - panelColour = BackgroundColour ?? Color4Extensions.FromHex("ff549a"); - textColour = TextColour ?? Color4.White; + setPanelColour(BackgroundColour ?? colours.Pink1); + usernameColour = colours.Pink1; } else + setPanelColour(BackgroundColour ?? colours.Blue4); + + usernameText.FadeColour(usernameColour, text_transition_duration, Easing.OutQuint); + + scorePanel.MoveToX(widthExtension ? 0 : left_panel_extension_width, panel_transition_duration, Easing.OutElastic); + leftLayer.ResizeWidthTo(widthExtension ? extended_left_panel_width : regular_left_panel_width, panel_transition_duration, Easing.OutElastic); + } + + private void setPanelColour(Color4 baseColour) + { + leftLayerGradient.Colour = ColourInfo.GradientVertical(baseColour.Opacity(0.2f), baseColour.Opacity(0.5f)); + rightLayerGradient.Colour = ColourInfo.GradientVertical(baseColour.Opacity(0.1f), baseColour.Opacity(0.3f)); + scorePanel.BorderColour = ColourInfo.GradientVertical(baseColour.Opacity(0.2f), baseColour); + } + + private void setPanelColourAsTracked() + { + leftLayerGradient.Colour = ColourInfo.GradientVertical(colours.Blue2.Opacity(0.3f), colours.Blue2); + rightLayerGradient.Colour = ColourInfo.GradientVertical(colours.Blue4.Opacity(0.25f), colours.Blue3.Opacity(0.6f)); + scorePanel.BorderColour = ColourInfo.GradientVertical(colours.Blue1.Opacity(0.2f), colours.Blue1); + } + + private partial class ScoreAvatar : CompositeDrawable + { + private readonly IUser? user; + + private Box placeholder = null!; + + public ScoreAvatar(IUser? user) { - panelColour = BackgroundColour ?? Color4Extensions.FromHex("3399cc"); - textColour = TextColour ?? Color4.White; + this.user = user; + + RelativeSizeAxes = Axes.Both; } - this.TransformTo(nameof(SizeContainerLeftPadding), widthExtension ? -top_player_left_width_extension : 0, panel_transition_duration, Easing.OutElastic); - } - - public float SizeContainerLeftPadding - { - get => backgroundPaddingAdjustContainer.Padding.Left; - set => backgroundPaddingAdjustContainer.Padding = new MarginPadding { Left = value }; - } - - private Color4 panelColour - { - set + [BackgroundDependencyLoader] + private void load() { - mainFillContainer.FadeColour(value, panel_transition_duration, Easing.OutQuint); - centralFill.FadeColour(value, panel_transition_duration, Easing.OutQuint); + InternalChild = placeholder = new Box + { + RelativeSizeAxes = Axes.Both, + Colour = OsuColour.Gray(0.1f), + }; } - } - private Color4 textColour - { - set + protected override void LoadComplete() { - scoreText.FadeColour(value, text_transition_duration, Easing.OutQuint); - accuracyText.FadeColour(value, text_transition_duration, Easing.OutQuint); - comboText.FadeColour(value, text_transition_duration, Easing.OutQuint); - usernameText.FadeColour(value, text_transition_duration, Easing.OutQuint); - positionText.FadeColour(value, text_transition_duration, Easing.OutQuint); + base.LoadComplete(); + + LoadComponentAsync(new DrawableAvatar(user), a => + { + placeholder.FadeOut(300, Easing.InQuint); + AddInternal(a); + }); } } }