diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a88f1320cd..d75f09f184 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -96,7 +96,7 @@ jobs: build-only-android: name: Build only (Android) - runs-on: windows-2019 + runs-on: windows-latest timeout-minutes: 60 steps: - name: Checkout @@ -114,10 +114,7 @@ jobs: dotnet-version: "8.0.x" - name: Install .NET workloads - # since windows image 20241113.3.0, not specifying a version here - # installs the .NET 7 version of android workload for very unknown reasons. - # revisit once we upgrade to .NET 9, it's probably fixed there. - run: dotnet workload install android --version (dotnet --version) + run: dotnet workload install android - name: Compile run: dotnet build -c Debug osu.Android.slnf diff --git a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableSwell.cs b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableSwell.cs index c130b5f366..286b16aa34 100644 --- a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableSwell.cs +++ b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableSwell.cs @@ -20,6 +20,7 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning { Anchor = Anchor.Centre, Origin = Anchor.Centre, + Scale = new osuTK.Vector2(0.5f), })); } diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs index 28617b35f6..6ad14c87d1 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs @@ -6,14 +6,9 @@ using System; using System.Linq; using JetBrains.Annotations; -using osu.Framework.Allocation; -using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Game.Graphics; using osu.Game.Rulesets.Objects.Drawables; -using osuTK.Graphics; -using osu.Framework.Graphics.Shapes; using osu.Framework.Input.Events; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Taiko.Skinning.Default; @@ -25,11 +20,6 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables { public partial class DrawableSwell : DrawableTaikoHitObject { - private const float target_ring_thick_border = 1.4f; - private const float target_ring_thin_border = 1f; - private const float target_ring_scale = 5f; - private const float inner_ring_alpha = 0.65f; - /// /// Offset away from the start time of the swell at which the ring starts appearing. /// @@ -38,9 +28,6 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables private Vector2 baseSize; private readonly Container ticks; - private readonly Container bodyContainer; - private readonly CircularContainer targetRing; - private readonly CircularContainer expandingRing; private double? lastPressHandleTime; @@ -51,6 +38,8 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables /// public bool MustAlternate { get; internal set; } = true; + public event Action UpdateHitProgress; + public DrawableSwell() : this(null) { @@ -61,87 +50,15 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables { FillMode = FillMode.Fit; - Content.Add(bodyContainer = new Container - { - RelativeSizeAxes = Axes.Both, - Depth = 1, - Children = new Drawable[] - { - expandingRing = new CircularContainer - { - Name = "Expanding ring", - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Alpha = 0, - RelativeSizeAxes = Axes.Both, - Blending = BlendingParameters.Additive, - Masking = true, - Children = new[] - { - new Box - { - RelativeSizeAxes = Axes.Both, - Alpha = inner_ring_alpha, - } - } - }, - targetRing = new CircularContainer - { - Name = "Target ring (thick border)", - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - RelativeSizeAxes = Axes.Both, - Masking = true, - BorderThickness = target_ring_thick_border, - Blending = BlendingParameters.Additive, - Children = new Drawable[] - { - new Box - { - RelativeSizeAxes = Axes.Both, - Alpha = 0, - AlwaysPresent = true - }, - new CircularContainer - { - Name = "Target ring (thin border)", - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - RelativeSizeAxes = Axes.Both, - Masking = true, - BorderThickness = target_ring_thin_border, - BorderColour = Color4.White, - Children = new[] - { - new Box - { - RelativeSizeAxes = Axes.Both, - Alpha = 0, - AlwaysPresent = true - } - } - } - } - } - } - }); - AddInternal(ticks = new Container { RelativeSizeAxes = Axes.Both }); } - [BackgroundDependencyLoader] - private void load(OsuColour colours) - { - expandingRing.Colour = colours.YellowLight; - targetRing.BorderColour = colours.YellowDark.Opacity(0.25f); - } - protected override SkinnableDrawable CreateMainPiece() => new SkinnableDrawable(new TaikoSkinComponentLookup(TaikoSkinComponents.Swell), - _ => new SwellCirclePiece + _ => new DefaultSwell { - // to allow for rotation transform Anchor = Anchor.Centre, Origin = Anchor.Centre, + RelativeSizeAxes = Axes.Both, }); protected override void RecreatePieces() @@ -208,16 +125,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables int numHits = ticks.Count(r => r.IsHit); - float completion = (float)numHits / HitObject.RequiredHits; - - expandingRing - .FadeTo(expandingRing.Alpha + Math.Clamp(completion / 16, 0.1f, 0.6f), 50) - .Then() - .FadeTo(completion / 8, 2000, Easing.OutQuint); - - MainPiece.Drawable.RotateTo((float)(completion * HitObject.Duration / 8), 4000, Easing.OutQuint); - - expandingRing.ScaleTo(1f + Math.Min(target_ring_scale - 1f, (target_ring_scale - 1f) * completion * 1.3f), 260, Easing.OutQuint); + UpdateHitProgress?.Invoke(numHits); if (numHits == HitObject.RequiredHits) ApplyMaxResult(); @@ -248,28 +156,21 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables } } - protected override void UpdateStartTimeStateTransforms() - { - base.UpdateStartTimeStateTransforms(); - - using (BeginDelayedSequence(-ring_appear_offset)) - targetRing.ScaleTo(target_ring_scale, 400, Easing.OutQuint); - } - protected override void UpdateHitStateTransforms(ArmedState state) { - const double transition_duration = 300; + base.UpdateHitStateTransforms(state); switch (state) { case ArmedState.Idle: - expandingRing.FadeTo(0); break; case ArmedState.Miss: + this.Delay(300).FadeOut(); + break; + case ArmedState.Hit: - this.FadeOut(transition_duration, Easing.Out); - bodyContainer.ScaleTo(1.4f, transition_duration); + this.Delay(660).FadeOut(); break; } } diff --git a/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonSwell.cs b/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonSwell.cs new file mode 100644 index 0000000000..3b3684d219 --- /dev/null +++ b/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonSwell.cs @@ -0,0 +1,20 @@ +// 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.Game.Rulesets.Taiko.Skinning.Default; + +namespace osu.Game.Rulesets.Taiko.Skinning.Argon +{ + public partial class ArgonSwell : DefaultSwell + { + protected override Drawable CreateCentreCircle() + { + return new ArgonSwellCirclePiece + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + }; + } + } +} diff --git a/osu.Game.Rulesets.Taiko/Skinning/Argon/TaikoArgonSkinTransformer.cs b/osu.Game.Rulesets.Taiko/Skinning/Argon/TaikoArgonSkinTransformer.cs index bfc9e8648d..26bb1900b9 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/Argon/TaikoArgonSkinTransformer.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/Argon/TaikoArgonSkinTransformer.cs @@ -69,7 +69,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Argon return new ArgonHitExplosion(taikoComponent.Component); case TaikoSkinComponents.Swell: - return new ArgonSwellCirclePiece(); + return new ArgonSwell(); } break; diff --git a/osu.Game.Rulesets.Taiko/Skinning/Default/DefaultSwell.cs b/osu.Game.Rulesets.Taiko/Skinning/Default/DefaultSwell.cs new file mode 100644 index 0000000000..ac72ba73b8 --- /dev/null +++ b/osu.Game.Rulesets.Taiko/Skinning/Default/DefaultSwell.cs @@ -0,0 +1,190 @@ +// 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.Extensions.Color4Extensions; +using osu.Framework.Extensions.ObjectExtensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Utils; +using osu.Game.Graphics; +using osu.Game.Rulesets.Objects.Drawables; +using osu.Game.Rulesets.Taiko.Objects; +using osu.Game.Rulesets.Taiko.Objects.Drawables; +using osuTK; +using osuTK.Graphics; + +namespace osu.Game.Rulesets.Taiko.Skinning.Default +{ + public partial class DefaultSwell : Container + { + private const float target_ring_thick_border = 1.4f; + private const float target_ring_thin_border = 1f; + private const float target_ring_scale = 5f; + private const float inner_ring_alpha = 0.65f; + + private DrawableSwell drawableSwell = null!; + + private readonly Container bodyContainer; + private readonly CircularContainer targetRing; + private readonly CircularContainer expandingRing; + private readonly Drawable centreCircle; + private int numHits; + + public DefaultSwell() + { + Anchor = Anchor.Centre; + Origin = Anchor.Centre; + RelativeSizeAxes = Axes.Both; + + Content.Add(bodyContainer = new Container + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.Both, + Depth = 1, + Children = new[] + { + expandingRing = new CircularContainer + { + Name = "Expanding ring", + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Alpha = 0, + RelativeSizeAxes = Axes.Both, + Blending = BlendingParameters.Additive, + Masking = true, + Children = new[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Alpha = inner_ring_alpha, + } + } + }, + targetRing = new CircularContainer + { + Name = "Target ring (thick border)", + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.Both, + Masking = true, + BorderThickness = target_ring_thick_border, + Blending = BlendingParameters.Additive, + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Alpha = 0, + AlwaysPresent = true + }, + new CircularContainer + { + Name = "Target ring (thin border)", + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.Both, + Masking = true, + BorderThickness = target_ring_thin_border, + BorderColour = Color4.White, + Children = new[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Alpha = 0, + AlwaysPresent = true + } + } + } + } + }, + centreCircle = CreateCentreCircle(), + } + }); + } + + [BackgroundDependencyLoader] + private void load(DrawableHitObject hitObject, OsuColour colours) + { + drawableSwell = (DrawableSwell)hitObject; + drawableSwell.UpdateHitProgress += animateSwellProgress; + drawableSwell.ApplyCustomUpdateState += updateStateTransforms; + + expandingRing.Colour = colours.YellowLight; + targetRing.BorderColour = colours.YellowDark.Opacity(0.25f); + } + + protected virtual Drawable CreateCentreCircle() + { + return new SwellCirclePiece + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + }; + } + + private void animateSwellProgress(int numHits) + { + this.numHits = numHits; + + float completion = (float)numHits / drawableSwell.HitObject.RequiredHits; + expandingRing.Alpha += Math.Clamp(completion / 16, 0.1f, 0.6f); + } + + protected override void Update() + { + base.Update(); + + float completion = (float)numHits / drawableSwell.HitObject.RequiredHits; + + centreCircle.Rotation = (float)Interpolation.DampContinuously(centreCircle.Rotation, + (float)(completion * drawableSwell.HitObject.Duration / 8), 500, Math.Abs(Time.Elapsed)); + expandingRing.Scale = new Vector2((float)Interpolation.DampContinuously(expandingRing.Scale.X, + 1f + Math.Min(target_ring_scale - 1f, (target_ring_scale - 1f) * completion * 1.3f), 35, Math.Abs(Time.Elapsed))); + expandingRing.Alpha = (float)Interpolation.DampContinuously(expandingRing.Alpha, completion / 16, 250, Math.Abs(Time.Elapsed)); + } + + private void updateStateTransforms(DrawableHitObject drawableHitObject, ArmedState state) + { + if (!(drawableHitObject is DrawableSwell)) + return; + + Swell swell = drawableSwell.HitObject; + + using (BeginAbsoluteSequence(swell.StartTime)) + { + if (state == ArmedState.Idle) + expandingRing.FadeTo(0); + + const double ring_appear_offset = 100; + + targetRing.Delay(ring_appear_offset).ScaleTo(target_ring_scale, 400, Easing.OutQuint); + } + + using (BeginAbsoluteSequence(drawableSwell.HitStateUpdateTime)) + { + const double transition_duration = 300; + + bodyContainer.FadeOut(transition_duration, Easing.OutQuad); + bodyContainer.ScaleTo(1.4f, transition_duration); + centreCircle.FadeOut(transition_duration, Easing.OutQuad); + } + } + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + + if (drawableSwell.IsNotNull()) + { + drawableSwell.UpdateHitProgress -= animateSwellProgress; + drawableSwell.ApplyCustomUpdateState -= updateStateTransforms; + } + } + } +} diff --git a/osu.Game.Rulesets.Taiko/Skinning/Default/SwellSymbolPiece.cs b/osu.Game.Rulesets.Taiko/Skinning/Default/SwellCirclePiece.cs similarity index 100% rename from osu.Game.Rulesets.Taiko/Skinning/Default/SwellSymbolPiece.cs rename to osu.Game.Rulesets.Taiko/Skinning/Default/SwellCirclePiece.cs diff --git a/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacySwell.cs b/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacySwell.cs new file mode 100644 index 0000000000..9f1b692984 --- /dev/null +++ b/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacySwell.cs @@ -0,0 +1,201 @@ +// 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.Allocation; +using osu.Framework.Graphics.Containers; +using osu.Game.Skinning; +using osu.Framework.Graphics.Sprites; +using osu.Game.Rulesets.Taiko.Objects.Drawables; +using osu.Game.Rulesets.Taiko.Objects; +using osu.Game.Audio; +using osuTK; +using osu.Game.Rulesets.Objects.Drawables; +using osu.Framework.Extensions.ObjectExtensions; +using System; +using System.Globalization; +using osu.Framework.Utils; + +namespace osu.Game.Rulesets.Taiko.Skinning.Legacy +{ + public partial class LegacySwell : Container + { + private const float scale_adjust = 768f / 480; + private static readonly Vector2 swell_display_position = new Vector2(250f, 100f); + + private DrawableSwell drawableSwell = null!; + + private Container bodyContainer = null!; + private Sprite warning = null!; + private Sprite spinnerCircle = null!; + private Sprite approachCircle = null!; + private Sprite clearAnimation = null!; + private SkinnableSound clearSample = null!; + private LegacySpriteText remainingHitsText = null!; + + private bool samplePlayed; + + private int numHits; + + public LegacySwell() + { + Anchor = Anchor.Centre; + Origin = Anchor.Centre; + RelativeSizeAxes = Axes.Both; + } + + [BackgroundDependencyLoader] + private void load(DrawableHitObject hitObject, ISkinSource skin) + { + Children = new Drawable[] + { + warning = new Sprite + { + Texture = skin.GetTexture("spinner-warning"), + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Scale = skin.GetTexture("spinner-warning") != null ? Vector2.One : new Vector2(0.18f), + }, + new Container + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Position = swell_display_position, // ballparked to be horizontally centred on 4:3 resolution + + Children = new Drawable[] + { + bodyContainer = new Container + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Alpha = 0, + + Children = new Drawable[] + { + spinnerCircle = new Sprite + { + Texture = skin.GetTexture("spinner-circle"), + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Scale = new Vector2(0.8f), + }, + approachCircle = new Sprite + { + Texture = skin.GetTexture("spinner-approachcircle"), + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Scale = new Vector2(1.86f * 0.8f), + Alpha = 0.8f, + }, + remainingHitsText = new LegacySpriteText(LegacyFont.Score) + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Position = new Vector2(0f, 130f), + Scale = Vector2.One, + }, + } + }, + clearAnimation = new Sprite + { + Texture = skin.GetTexture("spinner-osu"), + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Alpha = 0, + Y = -40, + }, + }, + }, + clearSample = new SkinnableSound(new SampleInfo("spinner-osu")), + }; + + drawableSwell = (DrawableSwell)hitObject; + drawableSwell.UpdateHitProgress += animateSwellProgress; + drawableSwell.ApplyCustomUpdateState += updateStateTransforms; + } + + private void animateSwellProgress(int numHits) + { + this.numHits = numHits; + remainingHitsText.Text = (drawableSwell.HitObject.RequiredHits - numHits).ToString(CultureInfo.InvariantCulture); + spinnerCircle.Scale = new Vector2(Math.Min(0.94f, spinnerCircle.Scale.X + 0.02f)); + } + + protected override void Update() + { + base.Update(); + + int requiredHits = drawableSwell.HitObject.RequiredHits; + int remainingHits = requiredHits - numHits; + remainingHitsText.Scale = new Vector2((float)Interpolation.DampContinuously( + remainingHitsText.Scale.X, 1.6f - (0.6f * ((float)remainingHits / requiredHits)), 17.5, Math.Abs(Time.Elapsed))); + + spinnerCircle.Rotation = (float)Interpolation.DampContinuously(spinnerCircle.Rotation, 180f * numHits, 130, Math.Abs(Time.Elapsed)); + spinnerCircle.Scale = new Vector2((float)Interpolation.DampContinuously( + spinnerCircle.Scale.X, 0.8f, 120, Math.Abs(Time.Elapsed))); + } + + private void updateStateTransforms(DrawableHitObject drawableHitObject, ArmedState state) + { + if (!(drawableHitObject is DrawableSwell)) + return; + + Swell swell = drawableSwell.HitObject; + + using (BeginAbsoluteSequence(swell.StartTime)) + { + if (state == ArmedState.Idle) + { + remainingHitsText.Text = $"{swell.RequiredHits}"; + samplePlayed = false; + } + + const double body_transition_duration = 200; + + warning.MoveTo(swell_display_position, body_transition_duration) + .ScaleTo(3, body_transition_duration, Easing.Out) + .FadeOut(body_transition_duration); + + bodyContainer.FadeIn(body_transition_duration); + approachCircle.ResizeTo(0.1f * 0.8f, swell.Duration); + } + + using (BeginAbsoluteSequence(drawableSwell.HitStateUpdateTime)) + { + const double clear_transition_duration = 300; + const double clear_fade_in = 120; + + bodyContainer.FadeOut(clear_transition_duration, Easing.OutQuad); + spinnerCircle.ScaleTo(spinnerCircle.Scale.X + 0.05f, clear_transition_duration, Easing.OutQuad); + + if (state == ArmedState.Hit) + { + if (!samplePlayed) + { + clearSample.Play(); + samplePlayed = true; + } + + clearAnimation + .MoveToOffset(new Vector2(0, -90 * scale_adjust), clear_fade_in * 2, Easing.Out) + .ScaleTo(0.4f) + .ScaleTo(1f, clear_fade_in * 2, Easing.Out) + .FadeIn() + .Delay(clear_fade_in * 3) + .FadeOut(clear_fade_in * 2.5); + } + } + } + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + + if (drawableSwell.IsNotNull()) + { + drawableSwell.UpdateHitProgress -= animateSwellProgress; + drawableSwell.ApplyCustomUpdateState -= updateStateTransforms; + } + } + } +} diff --git a/osu.Game.Rulesets.Taiko/Skinning/Legacy/TaikoLegacySkinTransformer.cs b/osu.Game.Rulesets.Taiko/Skinning/Legacy/TaikoLegacySkinTransformer.cs index 5bdb824f1c..c6221e0589 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/Legacy/TaikoLegacySkinTransformer.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/Legacy/TaikoLegacySkinTransformer.cs @@ -66,7 +66,9 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy return this.GetAnimation("sliderscorepoint", false, false); case TaikoSkinComponents.Swell: - // todo: support taiko legacy swell (https://github.com/ppy/osu/issues/13601). + if (GetTexture("spinner-circle") != null) + return new LegacySwell(); + return null; case TaikoSkinComponents.HitTarget: diff --git a/osu.Game/Collections/DrawableCollectionList.cs b/osu.Game/Collections/DrawableCollectionList.cs index 85af1d383d..c494b830d1 100644 --- a/osu.Game/Collections/DrawableCollectionList.cs +++ b/osu.Game/Collections/DrawableCollectionList.cs @@ -96,7 +96,6 @@ namespace osu.Game.Collections lastCreated = collections[changes.InsertedIndices[0]].ID; foreach (int i in changes.NewModifiedIndices) - { var updatedItem = collections[i]; diff --git a/osu.Game/Collections/DrawableCollectionListItem.cs b/osu.Game/Collections/DrawableCollectionListItem.cs index e86254329f..b0dd70227c 100644 --- a/osu.Game/Collections/DrawableCollectionListItem.cs +++ b/osu.Game/Collections/DrawableCollectionListItem.cs @@ -15,6 +15,7 @@ using osu.Framework.Localisation; using osu.Game.Database; using osu.Game.Graphics; using osu.Game.Graphics.Containers; +using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; using osu.Game.Overlays; using osuTK; @@ -27,7 +28,7 @@ namespace osu.Game.Collections /// public partial class DrawableCollectionListItem : OsuRearrangeableListItem>, IFilterable { - private const float item_height = 35; + private const float item_height = 45; private const float button_width = item_height * 0.75f; protected TextBox TextBox => content.TextBox; @@ -92,13 +93,11 @@ namespace osu.Game.Collections Padding = new MarginPadding { Right = collection.IsManaged ? button_width : 0 }, Children = new Drawable[] { - TextBox = new ItemTextBox + TextBox = new ItemTextBox(collection) { - RelativeSizeAxes = Axes.Both, - Size = Vector2.One, - CornerRadius = item_height / 2, + RelativeSizeAxes = Axes.X, + Height = item_height, CommitOnFocusLost = true, - PlaceholderText = collection.IsManaged ? string.Empty : "Create a new collection" }, } }, @@ -125,11 +124,57 @@ namespace osu.Game.Collections { protected override float LeftRightPadding => item_height / 2; + private const float count_text_size = 12; + + private readonly Live collection; + + private OsuSpriteText countText = null!; + + public ItemTextBox(Live collection) + { + this.collection = collection; + + CornerRadius = item_height / 2; + } + [BackgroundDependencyLoader] private void load(OsuColour colours) { BackgroundUnfocused = colours.GreySeaFoamDarker.Darken(0.5f); BackgroundFocused = colours.GreySeaFoam; + + if (collection.IsManaged) + { + TextContainer.Height *= (Height - count_text_size) / Height; + TextContainer.Margin = new MarginPadding { Bottom = count_text_size }; + + TextContainer.Add(countText = new OsuSpriteText + { + Anchor = Anchor.BottomLeft, + Origin = Anchor.TopLeft, + Depth = float.MinValue, + Font = OsuFont.Default.With(size: count_text_size, weight: FontWeight.SemiBold), + Margin = new MarginPadding { Top = 2, Left = 2 }, + Colour = colours.Yellow + }); + + // interestingly, it is not required to subscribe to change notifications on this collection at all for this to work correctly. + // the reasoning for this is that `DrawableCollectionList` already takes out a subscription on the set of all `BeatmapCollection`s - + // but that subscription does not only cover *changes to the set of collections* (i.e. addition/removal/rearrangement of collections), + // but also covers *changes to the properties of collections*, which `BeatmapMD5Hashes` is one. + // when a collection item changes due to `BeatmapMD5Hashes` changing, the list item is deleted and re-inserted, thus guaranteeing this to work correctly. + int count = collection.PerformRead(c => c.BeatmapMD5Hashes.Count); + + countText.Text = count == 1 + // Intentionally not localised until we have proper support for this (see https://github.com/ppy/osu-framework/pull/4918 + // but also in this case we want support for formatting a number within a string). + ? $"{count:#,0} item" + : $"{count:#,0} items"; + } + else + { + PlaceholderText = "Create a new collection"; + } } } @@ -210,7 +255,7 @@ namespace osu.Game.Collections private void deleteCollection() => collection.PerformWrite(c => c.Realm!.Remove(c)); } - public IEnumerable FilterTerms => [(LocalisableString)Model.Value.Name]; + public IEnumerable FilterTerms => Model.PerformRead(m => m.IsValid ? new[] { (LocalisableString)m.Name } : []); private bool matchingFilter = true; diff --git a/osu.Game/Input/Bindings/GlobalActionContainer.cs b/osu.Game/Input/Bindings/GlobalActionContainer.cs index 599ca6d6c1..e4dc2d503b 100644 --- a/osu.Game/Input/Bindings/GlobalActionContainer.cs +++ b/osu.Game/Input/Bindings/GlobalActionContainer.cs @@ -227,6 +227,10 @@ namespace osu.Game.Input.Bindings }; } + /// + /// IMPORTANT: New entries should always be added at the end of the enum, as key bindings are stored using the enum's numeric value and + /// changes in order would cause key bindings to get associated with the wrong action. + /// public enum GlobalAction { [LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.ToggleChat))]