1
0
mirror of https://github.com/ppy/osu.git synced 2024-11-11 10:18:22 +08:00

Merge pull request #18819 from peppy/difficulty-icon-refactor-pass

Refactor difficulty icons to not suck
This commit is contained in:
Dan Balasescu 2022-06-24 17:50:50 +09:00 committed by GitHub
commit c7e92f46f4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 82 additions and 232 deletions

View File

@ -872,10 +872,10 @@ namespace osu.Game.Tests.Visual.SongSelect
return set != null; return set != null;
}); });
FilterableGroupedDifficultyIcon groupIcon = null; GroupedDifficultyIcon groupIcon = null;
AddUntilStep("Find group icon for different ruleset", () => AddUntilStep("Find group icon for different ruleset", () =>
{ {
return (groupIcon = set.ChildrenOfType<FilterableGroupedDifficultyIcon>() return (groupIcon = set.ChildrenOfType<GroupedDifficultyIcon>()
.FirstOrDefault(icon => icon.Items.First().BeatmapInfo.Ruleset.OnlineID == 3)) != null; .FirstOrDefault(icon => icon.Items.First().BeatmapInfo.Ruleset.OnlineID == 3)) != null;
}); });

View File

@ -1,12 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
#nullable disable
using System;
using System.Collections.Generic;
using System.Threading;
using JetBrains.Annotations;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Extensions.Color4Extensions;
@ -16,19 +10,17 @@ using osu.Framework.Graphics.Cursor;
using osu.Framework.Graphics.Effects; using osu.Framework.Graphics.Effects;
using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.UserInterface;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Graphics.Containers; using osu.Game.Graphics.Containers;
using osu.Game.Rulesets; using osu.Game.Rulesets;
using osu.Game.Rulesets.Mods;
using osuTK; using osuTK;
using osuTK.Graphics; using osuTK.Graphics;
namespace osu.Game.Beatmaps.Drawables namespace osu.Game.Beatmaps.Drawables
{ {
public class DifficultyIcon : CompositeDrawable, IHasCustomTooltip<DifficultyIconTooltipContent> public class DifficultyIcon : CompositeDrawable, IHasCustomTooltip<DifficultyIconTooltipContent>, IHasCurrentValue<StarDifficulty>
{ {
private readonly Container iconContainer;
/// <summary> /// <summary>
/// Size of this difficulty icon. /// Size of this difficulty icon.
/// </summary> /// </summary>
@ -38,57 +30,53 @@ namespace osu.Game.Beatmaps.Drawables
set => iconContainer.Size = value; set => iconContainer.Size = value;
} }
[NotNull] /// <summary>
private readonly IBeatmapInfo beatmapInfo; /// Whether to display a tooltip on hover. Only works if a beatmap was provided at construction time.
/// </summary>
public bool ShowTooltip { get; set; } = true;
private readonly IBeatmapInfo? beatmap;
[CanBeNull]
private readonly IRulesetInfo ruleset; private readonly IRulesetInfo ruleset;
[CanBeNull] private Drawable background = null!;
private readonly IReadOnlyList<Mod> mods;
private readonly bool shouldShowTooltip; private readonly Container iconContainer;
private readonly bool performBackgroundDifficultyLookup; private readonly BindableWithCurrent<StarDifficulty> difficulty = new BindableWithCurrent<StarDifficulty>();
private readonly Bindable<StarDifficulty> difficultyBindable = new Bindable<StarDifficulty>(); public virtual Bindable<StarDifficulty> Current
private Drawable background;
/// <summary>
/// Creates a new <see cref="DifficultyIcon"/> with a given <see cref="RulesetInfo"/> and <see cref="Mod"/> combination.
/// </summary>
/// <param name="beatmapInfo">The beatmap to show the difficulty of.</param>
/// <param name="ruleset">The ruleset to show the difficulty with.</param>
/// <param name="mods">The mods to show the difficulty with.</param>
/// <param name="shouldShowTooltip">Whether to display a tooltip when hovered.</param>
/// <param name="performBackgroundDifficultyLookup">Whether to perform difficulty lookup (including calculation if necessary).</param>
public DifficultyIcon([NotNull] IBeatmapInfo beatmapInfo, [CanBeNull] IRulesetInfo ruleset, [CanBeNull] IReadOnlyList<Mod> mods, bool shouldShowTooltip = true, bool performBackgroundDifficultyLookup = true)
: this(beatmapInfo, shouldShowTooltip, performBackgroundDifficultyLookup)
{ {
this.ruleset = ruleset ?? beatmapInfo.Ruleset; get => difficulty.Current;
this.mods = mods ?? Array.Empty<Mod>(); set => difficulty.Current = value;
}
/// <summary>
/// Creates a new <see cref="DifficultyIcon"/> that follows the currently-selected ruleset and mods.
/// </summary>
/// <param name="beatmapInfo">The beatmap to show the difficulty of.</param>
/// <param name="shouldShowTooltip">Whether to display a tooltip when hovered.</param>
/// <param name="performBackgroundDifficultyLookup">Whether to perform difficulty lookup (including calculation if necessary).</param>
public DifficultyIcon([NotNull] IBeatmapInfo beatmapInfo, bool shouldShowTooltip = true, bool performBackgroundDifficultyLookup = true)
{
this.beatmapInfo = beatmapInfo ?? throw new ArgumentNullException(nameof(beatmapInfo));
this.shouldShowTooltip = shouldShowTooltip;
this.performBackgroundDifficultyLookup = performBackgroundDifficultyLookup;
AutoSizeAxes = Axes.Both;
InternalChild = iconContainer = new Container { Size = new Vector2(20f) };
} }
[Resolved] [Resolved]
private IRulesetStore rulesets { get; set; } private IRulesetStore rulesets { get; set; } = null!;
/// <summary>
/// Creates a new <see cref="DifficultyIcon"/>. Will use provided beatmap's <see cref="BeatmapInfo.StarRating"/> for initial value.
/// </summary>
/// <param name="beatmap">The beatmap to be displayed in the tooltip, and to be used for the initial star rating value.</param>
/// <param name="ruleset">An optional ruleset to be used for the icon display, in place of the beatmap's ruleset.</param>
public DifficultyIcon(IBeatmapInfo beatmap, IRulesetInfo? ruleset = null)
: this(ruleset ?? beatmap.Ruleset)
{
this.beatmap = beatmap;
Current.Value = new StarDifficulty(beatmap.StarRating, 0);
}
/// <summary>
/// Creates a new <see cref="DifficultyIcon"/> without an associated beatmap.
/// </summary>
/// <param name="ruleset">The ruleset to be used for the icon display.</param>
public DifficultyIcon(IRulesetInfo ruleset)
{
this.ruleset = ruleset;
AutoSizeAxes = Axes.Both;
InternalChild = iconContainer = new Container { Size = new Vector2(20f) };
}
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(OsuColour colours) private void load(OsuColour colours)
@ -111,7 +99,6 @@ namespace osu.Game.Beatmaps.Drawables
Child = background = new Box Child = background = new Box
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Colour = colours.ForStarDifficulty(beatmapInfo.StarRating) // Default value that will be re-populated once difficulty calculation completes
}, },
}, },
new ConstrainedIconContainer new ConstrainedIconContainer
@ -124,17 +111,12 @@ namespace osu.Game.Beatmaps.Drawables
}, },
}; };
if (performBackgroundDifficultyLookup) Current.BindValueChanged(difficulty => background.Colour = colours.ForStarDifficulty(difficulty.NewValue.Stars), true);
iconContainer.Add(new DelayedLoadUnloadWrapper(() => new DifficultyRetriever(beatmapInfo, ruleset, mods) { StarDifficulty = { BindTarget = difficultyBindable } }, 0));
else
difficultyBindable.Value = new StarDifficulty(beatmapInfo.StarRating, 0);
difficultyBindable.BindValueChanged(difficulty => background.Colour = colours.ForStarDifficulty(difficulty.NewValue.Stars));
} }
private Drawable getRulesetIcon() private Drawable getRulesetIcon()
{ {
int? onlineID = (ruleset ?? beatmapInfo.Ruleset).OnlineID; int? onlineID = ruleset.OnlineID;
if (onlineID >= 0 && rulesets.GetRuleset(onlineID.Value)?.CreateInstance() is Ruleset rulesetInstance) if (onlineID >= 0 && rulesets.GetRuleset(onlineID.Value)?.CreateInstance() is Ruleset rulesetInstance)
return rulesetInstance.CreateIcon(); return rulesetInstance.CreateIcon();
@ -142,51 +124,10 @@ namespace osu.Game.Beatmaps.Drawables
return new SpriteIcon { Icon = FontAwesome.Regular.QuestionCircle }; return new SpriteIcon { Icon = FontAwesome.Regular.QuestionCircle };
} }
ITooltip<DifficultyIconTooltipContent> IHasCustomTooltip<DifficultyIconTooltipContent>.GetCustomTooltip() => new DifficultyIconTooltip(); ITooltip<DifficultyIconTooltipContent> IHasCustomTooltip<DifficultyIconTooltipContent>.
GetCustomTooltip() => new DifficultyIconTooltip();
DifficultyIconTooltipContent IHasCustomTooltip<DifficultyIconTooltipContent>.TooltipContent => shouldShowTooltip ? new DifficultyIconTooltipContent(beatmapInfo, difficultyBindable) : null; DifficultyIconTooltipContent IHasCustomTooltip<DifficultyIconTooltipContent>.
TooltipContent => (ShowTooltip && beatmap != null ? new DifficultyIconTooltipContent(beatmap, Current) : null)!;
private class DifficultyRetriever : Component
{
public readonly Bindable<StarDifficulty> StarDifficulty = new Bindable<StarDifficulty>();
private readonly IBeatmapInfo beatmapInfo;
private readonly IRulesetInfo ruleset;
private readonly IReadOnlyList<Mod> mods;
private CancellationTokenSource difficultyCancellation;
[Resolved]
private BeatmapDifficultyCache difficultyCache { get; set; }
public DifficultyRetriever(IBeatmapInfo beatmapInfo, IRulesetInfo ruleset, IReadOnlyList<Mod> mods)
{
this.beatmapInfo = beatmapInfo;
this.ruleset = ruleset;
this.mods = mods;
}
private IBindable<StarDifficulty?> localStarDifficulty;
[BackgroundDependencyLoader]
private void load()
{
difficultyCancellation = new CancellationTokenSource();
localStarDifficulty = ruleset != null
? difficultyCache.GetBindableDifficulty(beatmapInfo, ruleset, mods, difficultyCancellation.Token)
: difficultyCache.GetBindableDifficulty(beatmapInfo, difficultyCancellation.Token);
localStarDifficulty.BindValueChanged(d =>
{
if (d.NewValue is StarDifficulty diff)
StarDifficulty.Value = diff;
});
}
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
difficultyCancellation?.Cancel();
}
}
} }
} }

View File

@ -1,39 +0,0 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
#nullable disable
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Graphics;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Rulesets;
using osuTK.Graphics;
namespace osu.Game.Beatmaps.Drawables
{
/// <summary>
/// A difficulty icon that contains a counter on the right-side of it.
/// </summary>
/// <remarks>
/// Used in cases when there are too many difficulty icons to show.
/// </remarks>
public class GroupedDifficultyIcon : DifficultyIcon
{
public GroupedDifficultyIcon(IEnumerable<IBeatmapInfo> beatmaps, IRulesetInfo ruleset, Color4 counterColour)
: base(beatmaps.OrderBy(b => b.StarRating).Last(), ruleset, null, false)
{
AddInternal(new OsuSpriteText
{
Anchor = Anchor.CentreRight,
Origin = Anchor.CentreRight,
Padding = new MarginPadding { Left = Size.X },
Margin = new MarginPadding { Left = 2, Right = 5 },
Font = OsuFont.GetFont(size: 14, weight: FontWeight.SemiBold),
Text = beatmaps.Count().ToString(),
Colour = counterColour,
});
}
}
}

View File

@ -274,8 +274,10 @@ namespace osu.Game.Overlays.BeatmapSet
Alpha = 0.5f Alpha = 0.5f
} }
}, },
icon = new DifficultyIcon(beatmapInfo, shouldShowTooltip: false) icon = new DifficultyIcon(beatmapInfo)
{ {
ShowTooltip = false,
Current = { Value = new StarDifficulty(beatmapInfo.StarRating, 0) },
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Origin = Anchor.Centre, Origin = Anchor.Centre,
Size = new Vector2(size - tile_icon_padding * 2), Size = new Vector2(size - tile_icon_padding * 2),

View File

@ -1,78 +0,0 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
#nullable disable
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Beatmaps.Drawables;
using osu.Game.Rulesets;
using osuTK;
namespace osu.Game.Screens.OnlinePlay.Components
{
public class ModeTypeInfo : OnlinePlayComposite
{
private const float height = 28;
private const float transition_duration = 100;
[Resolved]
private RulesetStore rulesets { get; set; }
private Container drawableRuleset;
public ModeTypeInfo()
{
AutoSizeAxes = Axes.Both;
}
[BackgroundDependencyLoader]
private void load()
{
Container gameTypeContainer;
InternalChild = new FillFlowContainer
{
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Horizontal,
Spacing = new Vector2(5f, 0f),
LayoutDuration = 100,
Children = new[]
{
drawableRuleset = new Container
{
AutoSizeAxes = Axes.Both,
},
gameTypeContainer = new Container
{
AutoSizeAxes = Axes.Both,
},
},
};
Type.BindValueChanged(type => gameTypeContainer.Child = new DrawableGameType(type.NewValue) { Size = new Vector2(height) }, true);
Playlist.CollectionChanged += (_, __) => updateBeatmap();
updateBeatmap();
}
private void updateBeatmap()
{
var item = Playlist.FirstOrDefault();
var ruleset = item == null ? null : rulesets.GetRuleset(item.RulesetID)?.CreateInstance();
if (item?.Beatmap != null && ruleset != null)
{
var mods = item.RequiredMods.Select(m => m.ToMod(ruleset)).ToArray();
drawableRuleset.FadeIn(transition_duration);
drawableRuleset.Child = new DifficultyIcon(item.Beatmap, ruleset.RulesetInfo, mods) { Size = new Vector2(height) };
}
else
drawableRuleset.FadeOut(transition_duration);
}
}
}

View File

@ -266,7 +266,7 @@ namespace osu.Game.Screens.OnlinePlay
} }
if (beatmap != null) if (beatmap != null)
difficultyIconContainer.Child = new DifficultyIcon(beatmap, ruleset, requiredMods, performBackgroundDifficultyLookup: false) { Size = new Vector2(icon_height) }; difficultyIconContainer.Child = new DifficultyIcon(beatmap, ruleset) { Size = new Vector2(icon_height) };
else else
difficultyIconContainer.Clear(); difficultyIconContainer.Clear();

View File

@ -53,7 +53,9 @@ namespace osu.Game.Screens.Select.Carousel
private Action<BeatmapInfo> hideRequested; private Action<BeatmapInfo> hideRequested;
private Triangles triangles; private Triangles triangles;
private StarCounter starCounter; private StarCounter starCounter;
private DifficultyIcon difficultyIcon;
[Resolved(CanBeNull = true)] [Resolved(CanBeNull = true)]
private BeatmapSetOverlay beatmapOverlay { get; set; } private BeatmapSetOverlay beatmapOverlay { get; set; }
@ -113,8 +115,9 @@ namespace osu.Game.Screens.Select.Carousel
Origin = Anchor.CentreLeft, Origin = Anchor.CentreLeft,
Children = new Drawable[] Children = new Drawable[]
{ {
new DifficultyIcon(beatmapInfo, shouldShowTooltip: false) difficultyIcon = new DifficultyIcon(beatmapInfo)
{ {
ShowTooltip = false,
Scale = new Vector2(1.8f), Scale = new Vector2(1.8f),
}, },
new FillFlowContainer new FillFlowContainer
@ -216,6 +219,8 @@ namespace osu.Game.Screens.Select.Carousel
starDifficultyBindable.BindValueChanged(d => starDifficultyBindable.BindValueChanged(d =>
{ {
starCounter.Current = (float)(d.NewValue?.Stars ?? 0); starCounter.Current = (float)(d.NewValue?.Stars ?? 0);
if (d.NewValue != null)
difficultyIcon.Current.Value = d.NewValue.Value;
}, true); }, true);
} }

View File

@ -19,7 +19,7 @@ namespace osu.Game.Screens.Select.Carousel
public readonly CarouselBeatmap Item; public readonly CarouselBeatmap Item;
public FilterableDifficultyIcon(CarouselBeatmap item) public FilterableDifficultyIcon(CarouselBeatmap item)
: base(item.BeatmapInfo, performBackgroundDifficultyLookup: false) : base(item.BeatmapInfo)
{ {
filtered.BindTo(item.Filtered); filtered.BindTo(item.Filtered);
filtered.ValueChanged += isFiltered => Schedule(() => this.FadeTo(isFiltered.NewValue ? 0.1f : 1, 100)); filtered.ValueChanged += isFiltered => Schedule(() => this.FadeTo(isFiltered.NewValue ? 0.1f : 1, 100));

View File

@ -8,23 +8,42 @@ using System.Linq;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Input.Events; using osu.Framework.Input.Events;
using osu.Game.Beatmaps.Drawables; using osu.Game.Beatmaps.Drawables;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Rulesets; using osu.Game.Rulesets;
using osuTK.Graphics; using osuTK.Graphics;
namespace osu.Game.Screens.Select.Carousel namespace osu.Game.Screens.Select.Carousel
{ {
public class FilterableGroupedDifficultyIcon : GroupedDifficultyIcon /// <summary>
/// A difficulty icon that contains a counter on the right-side of it.
/// </summary>
/// <remarks>
/// Used in cases when there are too many difficulty icons to show.
/// </remarks>
public class GroupedDifficultyIcon : DifficultyIcon
{ {
public readonly List<CarouselBeatmap> Items; public readonly List<CarouselBeatmap> Items;
public FilterableGroupedDifficultyIcon(List<CarouselBeatmap> items, RulesetInfo ruleset) public GroupedDifficultyIcon(List<CarouselBeatmap> items, RulesetInfo ruleset)
: base(items.Select(i => i.BeatmapInfo).ToList(), ruleset, Color4.White) : base(items.OrderBy(b => b.BeatmapInfo.StarRating).Last().BeatmapInfo, ruleset)
{ {
Items = items; Items = items;
foreach (var item in items) foreach (var item in items)
item.Filtered.BindValueChanged(_ => Scheduler.AddOnce(updateFilteredDisplay)); item.Filtered.BindValueChanged(_ => Scheduler.AddOnce(updateFilteredDisplay));
AddInternal(new OsuSpriteText
{
Anchor = Anchor.CentreRight,
Origin = Anchor.CentreRight,
Padding = new MarginPadding { Left = Size.X },
Margin = new MarginPadding { Left = 2, Right = 5 },
Font = OsuFont.GetFont(size: 14, weight: FontWeight.SemiBold),
Text = items.Count.ToString(),
Colour = Color4.White,
});
updateFilteredDisplay(); updateFilteredDisplay();
} }

View File

@ -93,7 +93,7 @@ namespace osu.Game.Screens.Select.Carousel
return beatmaps.Count > maximum_difficulty_icons return beatmaps.Count > maximum_difficulty_icons
? (IEnumerable<DifficultyIcon>)beatmaps.GroupBy(b => b.BeatmapInfo.Ruleset) ? (IEnumerable<DifficultyIcon>)beatmaps.GroupBy(b => b.BeatmapInfo.Ruleset)
.Select(group => new FilterableGroupedDifficultyIcon(group.ToList(), group.Last().BeatmapInfo.Ruleset)) .Select(group => new GroupedDifficultyIcon(group.ToList(), group.Last().BeatmapInfo.Ruleset))
: beatmaps.Select(b => new FilterableDifficultyIcon(b)); : beatmaps.Select(b => new FilterableDifficultyIcon(b));
} }
} }