From 9d3ee2a57340e6935ce6cd278f5cabbcd245b19f Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Thu, 17 Apr 2025 07:50:15 -0400 Subject: [PATCH] Add song select filter control --- .../TestSceneBeatmapFilterControl.cs | 31 +++ .../TestSceneDifficultyRangeSlider.cs | 69 +++++++ .../UserInterface/ShearedRangeSlider.cs | 2 + osu.Game/Localisation/UserInterfaceStrings.cs | 5 + osu.Game/Screens/SelectV2/FilterControl.cs | 182 ++++++++++++++++++ .../FilterControl_DifficultyRangeSlider.cs | 171 ++++++++++++++++ 6 files changed, 460 insertions(+) create mode 100644 osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapFilterControl.cs create mode 100644 osu.Game.Tests/Visual/SongSelectV2/TestSceneDifficultyRangeSlider.cs create mode 100644 osu.Game/Screens/SelectV2/FilterControl.cs create mode 100644 osu.Game/Screens/SelectV2/FilterControl_DifficultyRangeSlider.cs diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapFilterControl.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapFilterControl.cs new file mode 100644 index 0000000000..df7e5ee645 --- /dev/null +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneBeatmapFilterControl.cs @@ -0,0 +1,31 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using NUnit.Framework; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Game.Screens.SelectV2; + +namespace osu.Game.Tests.Visual.SongSelectV2 +{ + public partial class TestSceneBeatmapFilterControl : SongSelectComponentsTestScene + { + protected override Anchor ComponentAnchor => Anchor.TopRight; + protected override float InitialRelativeWidth => 0.7f; + + [SetUp] + public void SetUp() => Schedule(() => + { + Child = new Container + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Child = new FilterControl + { + State = { Value = Visibility.Visible }, + RelativeSizeAxes = Axes.X, + }, + }; + }); + } +} diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneDifficultyRangeSlider.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneDifficultyRangeSlider.cs new file mode 100644 index 0000000000..3cadbeb1e3 --- /dev/null +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneDifficultyRangeSlider.cs @@ -0,0 +1,69 @@ +// 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.Bindables; +using osu.Framework.Extensions.Color4Extensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Game.Overlays; +using osu.Game.Screens.SelectV2; +using osu.Game.Tests.Visual.UserInterface; +using osuTK; +using osuTK.Graphics; + +namespace osu.Game.Tests.Visual.SongSelectV2 +{ + public partial class TestSceneDifficultyRangeSlider : ThemeComparisonTestScene + { + private readonly BindableNumber customStart = new BindableNumber + { + MinValue = 0, + MaxValue = 10, + Precision = 0.1f + }; + + private readonly BindableNumber customEnd = new BindableNumber(10) + { + MinValue = 0, + MaxValue = 10, + Precision = 0.1f + }; + + public TestSceneDifficultyRangeSlider() + : base(false) + { + } + + protected override void LoadComplete() + { + base.LoadComplete(); + CreateThemedContent(OverlayColourScheme.Aquamarine); + } + + protected override Drawable CreateContent() => new Container + { + RelativeSizeAxes = Axes.Both, + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = Color4.Black.Opacity(0.5f), + }, + new FilterControl.DifficultyRangeSlider + { + Width = 600, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Scale = new Vector2(1), + LowerBound = customStart, + UpperBound = customEnd, + TooltipSuffix = "suffix", + NubWidth = 32, + MinRange = 0.1f, + } + } + }; + } +} diff --git a/osu.Game/Graphics/UserInterface/ShearedRangeSlider.cs b/osu.Game/Graphics/UserInterface/ShearedRangeSlider.cs index 7b90f35c56..3aaa143987 100644 --- a/osu.Game/Graphics/UserInterface/ShearedRangeSlider.cs +++ b/osu.Game/Graphics/UserInterface/ShearedRangeSlider.cs @@ -184,6 +184,8 @@ namespace osu.Game.Graphics.UserInterface private readonly ShearedRangeSlider rangeSlider; private readonly bool isUpper; + public new float NormalizedValue => base.NormalizedValue; + public new ShearedNub Nub => base.Nub; public string? DefaultString; diff --git a/osu.Game/Localisation/UserInterfaceStrings.cs b/osu.Game/Localisation/UserInterfaceStrings.cs index dceedca05c..95d0a4a9ec 100644 --- a/osu.Game/Localisation/UserInterfaceStrings.cs +++ b/osu.Game/Localisation/UserInterfaceStrings.cs @@ -84,6 +84,11 @@ namespace osu.Game.Localisation /// public static LocalisableString RightMouseScroll => new TranslatableString(getKey(@"right_mouse_scroll"), @"Right mouse drag to absolute scroll"); + /// + /// "Show converts" + /// + public static LocalisableString ShowConverts => new TranslatableString(getKey(@"show_converts"), @"Show converts"); + /// /// "Show converted beatmaps" /// diff --git a/osu.Game/Screens/SelectV2/FilterControl.cs b/osu.Game/Screens/SelectV2/FilterControl.cs new file mode 100644 index 0000000000..bb795e5717 --- /dev/null +++ b/osu.Game/Screens/SelectV2/FilterControl.cs @@ -0,0 +1,182 @@ +// 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.Game.Configuration; +using osu.Game.Graphics.Containers; +using osu.Game.Graphics.UserInterface; +using osu.Game.Graphics.UserInterfaceV2; +using osu.Game.Localisation; +using osu.Game.Resources.Localisation.Web; +using osu.Game.Screens.Select.Filter; +using osuTK; + +namespace osu.Game.Screens.SelectV2 +{ + public partial class FilterControl : OverlayContainer + { + // taken from draw visualiser. used for carousel alignment purposes. + public const float HEIGHT_FROM_SCREEN_TOP = 141 - corner_radius; + + private const float corner_radius = 8; + + private ShearedToggleButton showConvertedBeatmapsButton = null!; + private DifficultyRangeSlider difficultyRangeSlider = null!; + + [Resolved] + private OsuConfigManager config { get; set; } = null!; + + [BackgroundDependencyLoader] + private void load() + { + RelativeSizeAxes = Axes.X; + AutoSizeAxes = Axes.Y; + + Shear = OsuGame.SHEAR; + Margin = new MarginPadding { Top = -corner_radius, Right = -40 }; + + InternalChildren = new Drawable[] + { + new Container + { + RelativeSizeAxes = Axes.Both, + CornerRadius = corner_radius, + Masking = true, + Child = new WedgeBackground + { + Anchor = Anchor.TopRight, + Scale = new Vector2(-1, 1), + } + }, + new ReverseChildIDFillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Vertical, + Spacing = new Vector2(0f, 5f), + Padding = new MarginPadding { Top = corner_radius + 5, Bottom = 2, Right = 40f, Left = 2f }, + Children = new Drawable[] + { + new Container + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Shear = -OsuGame.SHEAR, + Child = new SongSelectSearchTextBox + { + RelativeSizeAxes = Axes.X, + HoldFocus = true, + // TODO: pending implementation + FilterText = "12345 matches", + }, + }, + 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), // can probably be removed? + new Dimension(GridSizeMode.AutoSize), + }, + Content = new[] + { + new[] + { + difficultyRangeSlider = new DifficultyRangeSlider + { + RelativeSizeAxes = Axes.X, + MinRange = 0.1f, + }, + Empty(), + showConvertedBeatmapsButton = new ShearedToggleButton + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Text = UserInterfaceStrings.ShowConverts, + Height = 30f, + }, + }, + } + }, + new GridContainer + { + RelativeSizeAxes = Axes.X, + Height = 30, + Shear = -OsuGame.SHEAR, + RowDimensions = new[] { new Dimension(GridSizeMode.AutoSize) }, + ColumnDimensions = new[] + { + new Dimension(maxSize: 210), + new Dimension(GridSizeMode.Absolute, 5), + new Dimension(maxSize: 230), + new Dimension(GridSizeMode.Absolute, 5), + new Dimension(), + }, + Content = new[] + { + new[] + { + new ShearedDropdown(SortStrings.Default) + { + RelativeSizeAxes = Axes.X, + Items = Enum.GetValues(), + }, + Empty(), + // todo: pending localisation + new ShearedDropdown("Group by") + { + RelativeSizeAxes = Axes.X, + Items = Enum.GetValues(), + }, + Empty(), + new CollectionDropdown + { + RelativeSizeAxes = Axes.X, + }, + } + } + }, + }, + } + }; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + difficultyRangeSlider.LowerBound = config.GetBindable(OsuSetting.DisplayStarsMinimum); + difficultyRangeSlider.UpperBound = config.GetBindable(OsuSetting.DisplayStarsMaximum); + config.BindWith(OsuSetting.ShowConvertedBeatmaps, showConvertedBeatmapsButton.Active); + } + + protected override void PopIn() + { + this.MoveToX(0, SongSelect.ENTER_DURATION, Easing.OutQuint) + .FadeIn(SongSelect.ENTER_DURATION / 3, Easing.In); + } + + protected override void PopOut() + { + this.MoveToX(150, SongSelect.ENTER_DURATION, Easing.OutQuint) + .FadeOut(SongSelect.ENTER_DURATION / 3, Easing.In); + } + + private partial class SongSelectSearchTextBox : ShearedFilterTextBox + { + protected override InnerSearchTextBox CreateInnerTextBox() => new InnerTextBox(); + + private partial class InnerTextBox : InnerFilterTextBox + { + public override bool HandleLeftRightArrows => false; + } + } + } +} diff --git a/osu.Game/Screens/SelectV2/FilterControl_DifficultyRangeSlider.cs b/osu.Game/Screens/SelectV2/FilterControl_DifficultyRangeSlider.cs new file mode 100644 index 0000000000..58c9c60460 --- /dev/null +++ b/osu.Game/Screens/SelectV2/FilterControl_DifficultyRangeSlider.cs @@ -0,0 +1,171 @@ +// 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.Extensions.Color4Extensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Colour; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Layout; +using osu.Game.Graphics; +using osu.Game.Graphics.UserInterface; +using osu.Game.Localisation; +using osu.Game.Overlays; +using osu.Game.Utils; +using osuTK.Graphics; + +namespace osu.Game.Screens.SelectV2 +{ + public partial class FilterControl + { + public partial class DifficultyRangeSlider : ShearedRangeSlider + { + private Container borderContainer = null!; + + private readonly LayoutValue drawSizeLayout = new LayoutValue(Invalidation.DrawSize); + + private static readonly (float, Color4)[] spectrum = OsuColour.STAR_DIFFICULTY_SPECTRUM + .Skip(1) + .Prepend((0.0f, OsuColour.STAR_DIFFICULTY_SPECTRUM.ElementAt(1).Item2)).ToArray(); + + public DifficultyRangeSlider() + : base("Star Rating") + { + NubWidth = ShearedNub.HEIGHT * 1.16f; + TooltipSuffix = "stars"; + DefaultStringLowerBound = "0.0"; + DefaultStringUpperBound = "∞"; + DefaultTooltipUpperBound = UserInterfaceStrings.NoLimit; + + AddLayout(drawSizeLayout); + } + + [BackgroundDependencyLoader] + private void load(OverlayColourProvider colourProvider, OsuColour colours) + { + SliderContainer.AddRange(new Drawable[] + { + new Container + { + Depth = 1, + RelativeSizeAxes = Axes.Both, + Shear = OsuGame.SHEAR, + CornerRadius = 5f, + Masking = true, + ChildrenEnumerable = spectrum.Zip(spectrum.Skip(1)) + .Select(p => new Box + { + RelativePositionAxes = Axes.X, + X = p.First.Item1 / 10f, + RelativeSizeAxes = Axes.Both, + Width = (p.Second.Item1 - p.First.Item1) / 10f, + Colour = ColourInfo.GradientHorizontal(p.First.Item2, p.Second.Item2), + }), + }, + borderContainer = new Container + { + Depth = -1, + RelativePositionAxes = Axes.X, + RelativeSizeAxes = Axes.Both, + Child = new Container + { + RelativeSizeAxes = Axes.Both, + BorderColour = colourProvider.Highlight1, + BorderThickness = 2, + Masking = true, + Shear = OsuGame.SHEAR, + CornerRadius = 5f, + Child = new Box + { + Colour = Color4.Transparent, + RelativeSizeAxes = Axes.Both, + } + }, + } + }); + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + LowerBoundSlider.Current.ValueChanged += _ => updateBorderDisplay(false); + UpperBoundSlider.Current.ValueChanged += _ => updateBorderDisplay(false); + } + + protected override void UpdateAfterChildren() + { + base.UpdateAfterChildren(); + + if (!drawSizeLayout.IsValid) + { + updateBorderDisplay(true); + drawSizeLayout.Validate(); + } + } + + private void updateBorderDisplay(bool instant) + { + float borderStart = LowerBoundSlider.NormalizedValue * LowerBoundSlider.UsableWidth / LowerBoundSlider.DrawWidth; + float borderEnd = UpperBoundSlider.NormalizedValue * UpperBoundSlider.UsableWidth / UpperBoundSlider.DrawWidth; + borderEnd += UpperBoundSlider.NubWidth / UpperBoundSlider.DrawWidth; + + borderContainer.MoveToX(borderStart, instant ? 0 : 250, Easing.OutQuint); + borderContainer.ResizeWidthTo(borderEnd - borderStart, instant ? 0 : 250, Easing.OutQuint); + } + + protected override BoundSliderBar CreateBoundSlider(bool isUpper) => new DifficultyBoundSliderBar(this, isUpper); + + private partial class DifficultyBoundSliderBar : BoundSliderBar + { + private readonly bool isUpper; + + protected override bool FocusIndicator => false; + + public DifficultyBoundSliderBar(ShearedRangeSlider slider, bool isUpper) + : base(slider, isUpper) + { + this.isUpper = isUpper; + } + + [Resolved] + private OsuColour colours { get; set; } = null!; + + protected override void LoadComplete() + { + base.LoadComplete(); + + if (isUpper) + { + LeftBox.Colour = OsuColour.Gray(0.4f).Opacity(0.2f); + RightBox.Colour = OsuColour.Gray(0.05f).Opacity(0.7f); + } + else + { + LeftBox.Colour = OsuColour.Gray(0.05f).Opacity(0.7f); + RightBox.Colour = OsuColour.Gray(0.4f).Opacity(0.2f); + } + } + + protected override void UpdateDisplay(double value) + { + Colour4 nubColour = ColourUtils.SampleFromLinearGradient(spectrum, (float)Math.Round(value, 2, MidpointRounding.AwayFromZero)); + nubColour = nubColour.Lighten(0.4f); + + if (value >= 8.0) + nubColour = colours.Gray4; + + Nub.AccentColour = nubColour; + Nub.GlowingAccentColour = nubColour.Lighten(0.2f); + Nub.ShadowColour = Color4.Black.Opacity(0.2f); + NubText.Colour = OsuColour.ForegroundTextColourFor(nubColour); + + base.UpdateDisplay(value); + } + } + } + } +}