// 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.Collections.Generic; using System.Threading; using JetBrains.Annotations; 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.Cursor; using osu.Framework.Graphics.Effects; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; using osuTK; using osuTK.Graphics; namespace osu.Game.Beatmaps.Drawables { public class DifficultyIcon : CompositeDrawable, IHasCustomTooltip { private readonly Container iconContainer; /// /// Size of this difficulty icon. /// public new Vector2 Size { get => iconContainer.Size; set => iconContainer.Size = value; } [NotNull] private readonly BeatmapInfo beatmap; [CanBeNull] private readonly RulesetInfo ruleset; [CanBeNull] private readonly IReadOnlyList mods; private readonly bool shouldShowTooltip; private readonly bool performBackgroundDifficultyLookup; private readonly Bindable difficultyBindable = new Bindable(); private Drawable background; /// /// Creates a new with a given and combination. /// /// The beatmap to show the difficulty of. /// The ruleset to show the difficulty with. /// The mods to show the difficulty with. /// Whether to display a tooltip when hovered. public DifficultyIcon([NotNull] BeatmapInfo beatmap, [CanBeNull] RulesetInfo ruleset, [CanBeNull] IReadOnlyList mods, bool shouldShowTooltip = true) : this(beatmap, shouldShowTooltip) { this.ruleset = ruleset ?? beatmap.Ruleset; this.mods = mods ?? Array.Empty(); } /// /// Creates a new that follows the currently-selected ruleset and mods. /// /// The beatmap to show the difficulty of. /// Whether to display a tooltip when hovered. /// Whether to perform difficulty lookup (including calculation if necessary). public DifficultyIcon([NotNull] BeatmapInfo beatmap, bool shouldShowTooltip = true, bool performBackgroundDifficultyLookup = true) { this.beatmap = beatmap ?? throw new ArgumentNullException(nameof(beatmap)); this.shouldShowTooltip = shouldShowTooltip; this.performBackgroundDifficultyLookup = performBackgroundDifficultyLookup; AutoSizeAxes = Axes.Both; InternalChild = iconContainer = new Container { Size = new Vector2(20f) }; } [BackgroundDependencyLoader] private void load(OsuColour colours) { iconContainer.Children = new Drawable[] { new CircularContainer { RelativeSizeAxes = Axes.Both, Anchor = Anchor.Centre, Origin = Anchor.Centre, Masking = true, EdgeEffect = new EdgeEffectParameters { Colour = Color4.Black.Opacity(0.06f), Type = EdgeEffectType.Shadow, Radius = 3, }, Child = background = new Box { RelativeSizeAxes = Axes.Both, Colour = colours.ForStarDifficulty(beatmap.StarDifficulty) // Default value that will be re-populated once difficulty calculation completes }, }, new ConstrainedIconContainer { Anchor = Anchor.Centre, Origin = Anchor.Centre, RelativeSizeAxes = Axes.Both, // the null coalesce here is only present to make unit tests work (ruleset dlls aren't copied correctly for testing at the moment) Icon = (ruleset ?? beatmap.Ruleset)?.CreateInstance()?.CreateIcon() ?? new SpriteIcon { Icon = FontAwesome.Regular.QuestionCircle } }, }; if (performBackgroundDifficultyLookup) iconContainer.Add(new DelayedLoadUnloadWrapper(() => new DifficultyRetriever(beatmap, ruleset, mods) { StarDifficulty = { BindTarget = difficultyBindable } }, 0)); else difficultyBindable.Value = new StarDifficulty(beatmap.StarDifficulty, 0); difficultyBindable.BindValueChanged(difficulty => background.Colour = colours.ForStarDifficulty(difficulty.NewValue.Stars)); } ITooltip IHasCustomTooltip.GetCustomTooltip() => new DifficultyIconTooltip(); DifficultyIconTooltipContent IHasCustomTooltip.TooltipContent => shouldShowTooltip ? new DifficultyIconTooltipContent(beatmap, difficultyBindable) : null; private class DifficultyRetriever : Component { public readonly Bindable StarDifficulty = new Bindable(); private readonly BeatmapInfo beatmap; private readonly RulesetInfo ruleset; private readonly IReadOnlyList mods; private CancellationTokenSource difficultyCancellation; [Resolved] private BeatmapDifficultyCache difficultyCache { get; set; } public DifficultyRetriever(BeatmapInfo beatmap, RulesetInfo ruleset, IReadOnlyList mods) { this.beatmap = beatmap; this.ruleset = ruleset; this.mods = mods; } private IBindable localStarDifficulty; [BackgroundDependencyLoader] private void load() { difficultyCancellation = new CancellationTokenSource(); localStarDifficulty = ruleset != null ? difficultyCache.GetBindableDifficulty(beatmap, ruleset, mods, difficultyCancellation.Token) : difficultyCache.GetBindableDifficulty(beatmap, 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(); } } } }