From cb89684bb4dd8e7d73fdf6213d7a43eb36d25473 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Wed, 5 Feb 2025 08:16:51 -0500 Subject: [PATCH] Implement group panel design --- .../TestSceneBeatmapCarouselGroupPanel.cs | 80 +++++ osu.Game/Screens/SelectV2/BeatmapCarousel.cs | 1 + osu.Game/Screens/SelectV2/GroupPanel.cs | 220 ++++++++++--- osu.Game/Screens/SelectV2/StarsGroupPanel.cs | 288 ++++++++++++++++++ 4 files changed, 541 insertions(+), 48 deletions(-) create mode 100644 osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselGroupPanel.cs create mode 100644 osu.Game/Screens/SelectV2/StarsGroupPanel.cs diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselGroupPanel.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselGroupPanel.cs new file mode 100644 index 000000000000..eea387011711 --- /dev/null +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapCarouselGroupPanel.cs @@ -0,0 +1,80 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Game.Screens.SelectV2; +using osu.Game.Tests.Visual.UserInterface; +using osuTK; + +namespace osu.Game.Tests.Visual.SongSelectV2 +{ + public partial class TestSceneBeatmapCarouselGroupPanel : ThemeComparisonTestScene + { + public TestSceneBeatmapCarouselGroupPanel() + : base(false) + { + } + + protected override Drawable CreateContent() + { + return new FillFlowContainer + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Width = 0.5f, + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Vertical, + Spacing = new Vector2(0f, 5f), + Children = new Drawable[] + { + new GroupPanel + { + Item = new CarouselItem(new GroupDefinition("Group A")) + }, + new GroupPanel + { + Item = new CarouselItem(new GroupDefinition("Group A")), + KeyboardSelected = { Value = true } + }, + new GroupPanel + { + Item = new CarouselItem(new GroupDefinition("Group A")), + Selected = { Value = true } + }, + new GroupPanel + { + Item = new CarouselItem(new GroupDefinition("Group A")), + KeyboardSelected = { Value = true }, + Selected = { Value = true } + }, + new StarsGroupPanel + { + Item = new CarouselItem(new StarsGroupDefinition(1)) + }, + new StarsGroupPanel + { + Item = new CarouselItem(new StarsGroupDefinition(3)), + }, + new StarsGroupPanel + { + Item = new CarouselItem(new StarsGroupDefinition(5)), + }, + new StarsGroupPanel + { + Item = new CarouselItem(new StarsGroupDefinition(7)), + }, + new StarsGroupPanel + { + Item = new CarouselItem(new StarsGroupDefinition(8)), + }, + new StarsGroupPanel + { + Item = new CarouselItem(new StarsGroupDefinition(9)), + }, + } + }; + } + } +} diff --git a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs index 12660d864221..a49dcdd86c28 100644 --- a/osu.Game/Screens/SelectV2/BeatmapCarousel.cs +++ b/osu.Game/Screens/SelectV2/BeatmapCarousel.cs @@ -264,4 +264,5 @@ protected override Drawable GetDrawableForDisplay(CarouselItem item) } public record GroupDefinition(string Title); + public record StarsGroupDefinition(int StarNumber); } diff --git a/osu.Game/Screens/SelectV2/GroupPanel.cs b/osu.Game/Screens/SelectV2/GroupPanel.cs index df930a311128..8995b932908b 100644 --- a/osu.Game/Screens/SelectV2/GroupPanel.cs +++ b/osu.Game/Screens/SelectV2/GroupPanel.cs @@ -7,10 +7,14 @@ using osu.Framework.Bindables; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Pooling; using osu.Framework.Graphics.Shapes; using osu.Framework.Input.Events; +using osu.Game.Graphics; using osu.Game.Graphics.Sprites; +using osu.Game.Graphics.UserInterface; +using osu.Game.Overlays; using osuTK; using osuTK.Graphics; @@ -18,15 +22,20 @@ namespace osu.Game.Screens.SelectV2 { public partial class GroupPanel : PoolableDrawable, ICarouselPanel { - public const float HEIGHT = CarouselItem.DEFAULT_HEIGHT * 2; + public const float HEIGHT = CarouselItem.DEFAULT_HEIGHT; + + private const float glow_offset = 10f; // extra space for the edge effect to not be cutoff by the right edge of the carousel. + private const float preselected_x_offset = 25f; + private const float selected_x_offset = 50f; + + private const float duration = 500; [Resolved] - private BeatmapCarousel carousel { get; set; } = null!; + private BeatmapCarousel? carousel { get; set; } private Box activationFlash = null!; - private OsuSpriteText text = null!; - - private Box box = null!; + private OsuSpriteText titleText = null!; + private Box hoverLayer = null!; public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) { @@ -39,56 +48,128 @@ public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) } [BackgroundDependencyLoader] - private void load() + private void load(OverlayColourProvider colourProvider, OsuColour colours) { - Size = new Vector2(500, HEIGHT); - Masking = true; + Anchor = Anchor.TopRight; + Origin = Anchor.TopRight; + RelativeSizeAxes = Axes.X; + Height = HEIGHT; - InternalChildren = new Drawable[] + InternalChild = new Container { - box = new Box - { - Colour = Color4.DarkBlue.Darken(5), - Alpha = 0.8f, - RelativeSizeAxes = Axes.Both, - }, - activationFlash = new Box - { - Colour = Color4.White, - Blending = BlendingParameters.Additive, - Alpha = 0, - RelativeSizeAxes = Axes.Both, - }, - text = new OsuSpriteText + RelativeSizeAxes = Axes.Both, + CornerRadius = 10f, + Masking = true, + Children = new Drawable[] { - Padding = new MarginPadding(5), - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, + new Container + { + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding { Left = 10f }, + Child = new Container + { + RelativeSizeAxes = Axes.Both, + CornerRadius = 10f, + Masking = true, + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = colourProvider.Background6, + }, + } + } + }, + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = colourProvider.Background3, + }, + new Container + { + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding { Left = 10f }, + Child = new Container + { + RelativeSizeAxes = Axes.Both, + CornerRadius = 10f, + Masking = true, + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = colourProvider.Background5, + }, + titleText = new OsuSpriteText + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + X = 10f, + }, + new CircularContainer + { + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, + Size = new Vector2(50f, 14f), + Margin = new MarginPadding { Right = 30f }, + Masking = true, + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = Color4.Black.Opacity(0.7f), + }, + new OsuSpriteText + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Font = OsuFont.Torus.With(size: 14.4f, weight: FontWeight.Bold), + // TODO: requires Carousel/CarouselItem-side implementation + Text = "43", + UseFullGlyphHeight = false, + } + }, + }, + } + } + }, + activationFlash = new Box + { + Colour = Color4.White, + Blending = BlendingParameters.Additive, + Alpha = 0, + RelativeSizeAxes = Axes.Both, + }, + hoverLayer = new Box + { + Colour = colours.Blue.Opacity(0.1f), + Alpha = 0, + Blending = BlendingParameters.Additive, + RelativeSizeAxes = Axes.Both, + }, + new HoverSounds(), } }; + } - Selected.BindValueChanged(value => - { - activationFlash.FadeTo(value.NewValue ? 0.2f : 0, 500, Easing.OutQuint); - }); + protected override void LoadComplete() + { + base.LoadComplete(); - Expanded.BindValueChanged(value => - { - box.FadeColour(value.NewValue ? Color4.SkyBlue : Color4.DarkBlue.Darken(5), 500, Easing.OutQuint); - }); + Expanded.BindValueChanged(_ => updateExpandedDisplay(), true); + KeyboardSelected.BindValueChanged(_ => updateKeyboardSelectedDisplay(), true); + } - KeyboardSelected.BindValueChanged(value => - { - if (value.NewValue) - { - BorderThickness = 5; - BorderColour = Color4.Pink; - } - else - { - BorderThickness = 0; - } - }); + private void updateExpandedDisplay() + { + updatePanelPosition(); + + // todo: figma shares no extra visual feedback on this. + + activationFlash.FadeTo(0.2f).FadeTo(0f, 500, Easing.OutQuint); } protected override void PrepareForUse() @@ -99,17 +180,60 @@ protected override void PrepareForUse() GroupDefinition group = (GroupDefinition)Item.Model; - text.Text = group.Title; + titleText.Text = group.Title; this.FadeInFromZero(500, Easing.OutQuint); } protected override bool OnClick(ClickEvent e) { - carousel.CurrentSelection = Item!.Model; + if (carousel != null) + carousel.CurrentSelection = Item!.Model; + return true; } + private void updateKeyboardSelectedDisplay() + { + updatePanelPosition(); + updateHover(); + } + + private void updatePanelPosition() + { + float x = glow_offset + selected_x_offset + preselected_x_offset; + + if (Expanded.Value) + x -= selected_x_offset; + + if (KeyboardSelected.Value) + x -= preselected_x_offset; + + this.TransformTo(nameof(Padding), new MarginPadding { Left = x }, duration, Easing.OutQuint); + } + + private void updateHover() + { + bool hovered = IsHovered || KeyboardSelected.Value; + + if (hovered) + hoverLayer.FadeIn(100, Easing.OutQuint); + else + hoverLayer.FadeOut(1000, Easing.OutQuint); + } + + protected override bool OnHover(HoverEvent e) + { + updateHover(); + return true; + } + + protected override void OnHoverLost(HoverLostEvent e) + { + updateHover(); + base.OnHoverLost(e); + } + #region ICarouselPanel public CarouselItem? Item { get; set; } diff --git a/osu.Game/Screens/SelectV2/StarsGroupPanel.cs b/osu.Game/Screens/SelectV2/StarsGroupPanel.cs new file mode 100644 index 000000000000..8ebf3fc7e83c --- /dev/null +++ b/osu.Game/Screens/SelectV2/StarsGroupPanel.cs @@ -0,0 +1,288 @@ +// 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 osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Extensions.Color4Extensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Pooling; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Input.Events; +using osu.Game.Beatmaps; +using osu.Game.Beatmaps.Drawables; +using osu.Game.Graphics; +using osu.Game.Graphics.Sprites; +using osu.Game.Graphics.UserInterface; +using osu.Game.Overlays; +using osuTK; +using osuTK.Graphics; + +namespace osu.Game.Screens.SelectV2 +{ + public partial class StarsGroupPanel : PoolableDrawable, ICarouselPanel + { + public const float HEIGHT = CarouselItem.DEFAULT_HEIGHT; + + private const float glow_offset = 10f; // extra space for the edge effect to not be cutoff by the right edge of the carousel. + private const float preselected_x_offset = 25f; + private const float expanded_x_offset = 50f; + + private const float duration = 500; + + [Resolved] + private BeatmapCarousel? carousel { get; set; } + + [Resolved] + private OsuColour colours { get; set; } = null!; + + [Resolved] + private OverlayColourProvider colourProvider { get; set; } = null!; + + private Box activationFlash = null!; + private Box outerLayer = null!; + private Box innerLayer = null!; + private StarRatingDisplay starRatingDisplay = null!; + private StarCounter starCounter = null!; + private Box hoverLayer = null!; + + public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) + { + var inputRectangle = DrawRectangle; + + // Cover a gap introduced by the spacing between a GroupPanel and a BeatmapPanel either below/above it. + inputRectangle = inputRectangle.Inflate(new MarginPadding { Vertical = BeatmapCarousel.SPACING / 2f }); + + return inputRectangle.Contains(ToLocalSpace(screenSpacePos)); + } + + [BackgroundDependencyLoader] + private void load() + { + Anchor = Anchor.TopRight; + Origin = Anchor.TopRight; + RelativeSizeAxes = Axes.X; + Height = HEIGHT; + + InternalChild = new Container + { + RelativeSizeAxes = Axes.Both, + CornerRadius = 10f, + Masking = true, + Children = new Drawable[] + { + new Container + { + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding { Left = 10f }, + Child = new Container + { + RelativeSizeAxes = Axes.Both, + CornerRadius = 10f, + Masking = true, + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = colourProvider.Background6, + }, + } + } + }, + outerLayer = new Box + { + RelativeSizeAxes = Axes.Both, + Colour = colourProvider.Background3, + }, + new Container + { + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding { Left = 10f }, + Child = new Container + { + RelativeSizeAxes = Axes.Both, + CornerRadius = 10f, + Masking = true, + Children = new Drawable[] + { + innerLayer = new Box + { + RelativeSizeAxes = Axes.Both, + Colour = Color4.Black.Opacity(0.2f), + }, + new FillFlowContainer + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + AutoSizeAxes = Axes.Both, + Spacing = new Vector2(10f, 0f), + Margin = new MarginPadding { Left = 10f }, + Children = new Drawable[] + { + starRatingDisplay = new StarRatingDisplay(default, StarRatingDisplaySize.Small) + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + }, + starCounter = new StarCounter + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Scale = new Vector2(8f / 20f), + }, + } + }, + new CircularContainer + { + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, + Size = new Vector2(50f, 14f), + Margin = new MarginPadding { Right = 30f }, + Masking = true, + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = Color4.Black.Opacity(0.7f), + }, + new OsuSpriteText + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Font = OsuFont.Torus.With(size: 14.4f, weight: FontWeight.Bold), + // TODO: requires Carousel/CarouselItem-side implementation + Text = "43", + UseFullGlyphHeight = false, + } + }, + }, + } + } + }, + activationFlash = new Box + { + Colour = Color4.White, + Blending = BlendingParameters.Additive, + Alpha = 0, + RelativeSizeAxes = Axes.Both, + }, + hoverLayer = new Box + { + Colour = colours.Blue.Opacity(0.1f), + Alpha = 0, + Blending = BlendingParameters.Additive, + RelativeSizeAxes = Axes.Both, + }, + new HoverSounds(), + } + }; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + Expanded.BindValueChanged(_ => updateExpandedDisplay(), true); + KeyboardSelected.BindValueChanged(_ => updateKeyboardSelectedDisplay(), true); + } + + private void updateExpandedDisplay() + { + updatePanelPosition(); + + // todo: figma shares no extra visual feedback on this. + + activationFlash.FadeTo(0.2f).FadeTo(0f, 500, Easing.OutQuint); + } + + protected override void PrepareForUse() + { + base.PrepareForUse(); + + Debug.Assert(Item != null); + + StarsGroupDefinition group = (StarsGroupDefinition)Item.Model; + + Color4 colour = group.StarNumber >= 9 ? OsuColour.Gray(0.2f) : colours.ForStarDifficulty(group.StarNumber); + Color4 contentColour = group.StarNumber >= 7 ? colours.Orange1 : colourProvider.Background5; + + outerLayer.Colour = colour; + starCounter.Colour = contentColour; + + starRatingDisplay.Current.Value = new StarDifficulty(group.StarNumber, 0); + starCounter.Current = group.StarNumber; + + this.FadeInFromZero(500, Easing.OutQuint); + } + + protected override bool OnClick(ClickEvent e) + { + if (carousel != null) + carousel.CurrentSelection = Item!.Model; + + return true; + } + + private void updateKeyboardSelectedDisplay() + { + updatePanelPosition(); + updateHover(); + } + + private void updatePanelPosition() + { + float x = glow_offset + expanded_x_offset + preselected_x_offset; + + if (Expanded.Value) + x -= expanded_x_offset; + + if (KeyboardSelected.Value) + x -= preselected_x_offset; + + this.TransformTo(nameof(Padding), new MarginPadding { Left = x }, duration, Easing.OutQuint); + } + + private void updateHover() + { + bool hovered = IsHovered || KeyboardSelected.Value; + + if (hovered) + hoverLayer.FadeIn(100, Easing.OutQuint); + else + hoverLayer.FadeOut(1000, Easing.OutQuint); + } + + protected override bool OnHover(HoverEvent e) + { + updateHover(); + return true; + } + + protected override void OnHoverLost(HoverLostEvent e) + { + updateHover(); + base.OnHoverLost(e); + } + + #region ICarouselPanel + + public CarouselItem? Item { get; set; } + public BindableBool Selected { get; } = new BindableBool(); + public BindableBool Expanded { get; } = new BindableBool(); + public BindableBool KeyboardSelected { get; } = new BindableBool(); + + public double DrawYPosition { get; set; } + + public void Activated() + { + // sets should never be activated. + throw new InvalidOperationException(); + } + + #endregion + } +}