1
0
mirror of https://github.com/ppy/osu.git synced 2026-05-27 16:10:16 +08:00
Files
osu-lazer/osu.Game.Tests/Visual/SongSelect/BeatmapCarouselV2TestScene.cs
T
Dean Herbert f9112066d3 Fix carousel handling of bleed areas
The idea of specifying "bleed" is to make the carousel aware of its
vertical display area. The top bleed is under the filter control; bottom
beneath the toolbar. At the end of the day, the point of panel X offset
incursion, and the scroll target for current selection, should be at the
centre of the screen.

The fixes match code which already exists in the previous
implementation. Basically, without incorporating `BleedTop` into
calculations a second time, the centre position would not match
expectations (of being the centre including bleed).
2025-04-11 00:05:51 +09:00

300 lines
13 KiB
C#

// 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.
using System;
using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Testing;
using osu.Framework.Utils;
using osu.Game.Beatmaps;
using osu.Game.Database;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Overlays;
using osu.Game.Screens.Select;
using osu.Game.Screens.SelectV2;
using osu.Game.Tests.Beatmaps;
using osu.Game.Tests.Resources;
using osuTK;
using osuTK.Graphics;
using osuTK.Input;
using BeatmapCarousel = osu.Game.Screens.SelectV2.BeatmapCarousel;
namespace osu.Game.Tests.Visual.SongSelect
{
public abstract partial class BeatmapCarouselV2TestScene : OsuManualInputManagerTestScene
{
protected readonly BindableList<BeatmapSetInfo> BeatmapSets = new BindableList<BeatmapSetInfo>();
protected BeatmapCarousel Carousel = null!;
protected OsuScrollContainer<Drawable> Scroll => Carousel.ChildrenOfType<OsuScrollContainer<Drawable>>().Single();
[Cached(typeof(BeatmapStore))]
private BeatmapStore store;
[Cached]
private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Aquamarine);
private OsuTextFlowContainer stats = null!;
private int beatmapCount;
protected BeatmapCarouselV2TestScene()
{
store = new TestBeatmapStore
{
BeatmapSets = { BindTarget = BeatmapSets }
};
BeatmapSets.BindCollectionChanged((_, _) => beatmapCount = BeatmapSets.Sum(s => s.Beatmaps.Count));
Scheduler.AddDelayed(updateStats, 100, true);
}
protected void CreateCarousel()
{
AddStep("create components", () =>
{
Box topBox;
Children = new Drawable[]
{
new GridContainer
{
RelativeSizeAxes = Axes.Both,
ColumnDimensions = new[]
{
new Dimension(GridSizeMode.Relative, 1),
},
RowDimensions = new[]
{
new Dimension(GridSizeMode.Absolute, 200),
new Dimension(),
new Dimension(GridSizeMode.Absolute, 200),
},
Content = new[]
{
new Drawable[]
{
topBox = new Box
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Colour = Color4.Cyan,
RelativeSizeAxes = Axes.Both,
Alpha = 0.4f,
},
},
new Drawable[]
{
Carousel = new BeatmapCarousel
{
BleedTop = 200,
BleedBottom = 200,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Width = 800,
RelativeSizeAxes = Axes.Y,
},
},
new[]
{
new Box
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Colour = Color4.Cyan,
RelativeSizeAxes = Axes.Both,
Alpha = 0.4f,
},
topBox.CreateProxy(),
}
}
},
stats = new OsuTextFlowContainer
{
AutoSizeAxes = Axes.Both,
Padding = new MarginPadding(10),
TextAnchor = Anchor.CentreLeft,
},
};
});
}
protected void SortBy(FilterCriteria criteria) => AddStep($"sort:{criteria.Sort} group:{criteria.Group}", () => Carousel.Filter(criteria));
protected void WaitForDrawablePanels() => AddUntilStep("drawable panels loaded", () => Carousel.ChildrenOfType<ICarouselPanel>().Count(), () => Is.GreaterThan(0));
protected void WaitForSorting() => AddUntilStep("sorting finished", () => Carousel.IsFiltering, () => Is.False);
protected void WaitForScrolling() => AddUntilStep("scroll finished", () => Scroll.Current, () => Is.EqualTo(Scroll.Target));
protected void SelectNextPanel() => AddStep("select next panel", () => InputManager.Key(Key.Down));
protected void SelectPrevPanel() => AddStep("select prev panel", () => InputManager.Key(Key.Up));
protected void SelectNextGroup() => AddStep("select next group", () => InputManager.Key(Key.Right));
protected void SelectPrevGroup() => AddStep("select prev group", () => InputManager.Key(Key.Left));
protected void Select() => AddStep("select", () => InputManager.Key(Key.Enter));
protected void CheckNoSelection() => AddAssert("has no selection", () => Carousel.CurrentSelection, () => Is.Null);
protected void CheckHasSelection() => AddAssert("has selection", () => Carousel.CurrentSelection, () => Is.Not.Null);
protected ICarouselPanel? GetSelectedPanel() => Carousel.ChildrenOfType<ICarouselPanel>().SingleOrDefault(p => p.Selected.Value);
protected ICarouselPanel? GetKeyboardSelectedPanel() => Carousel.ChildrenOfType<ICarouselPanel>().SingleOrDefault(p => p.KeyboardSelected.Value);
protected void WaitForGroupSelection(int group, int panel)
{
AddUntilStep($"selected is group{group} panel{panel}", () =>
{
var groupingFilter = Carousel.Filters.OfType<BeatmapCarouselFilterGrouping>().Single();
GroupDefinition g = groupingFilter.GroupItems.Keys.ElementAt(group);
// offset by one because the group itself is included in the items list.
CarouselItem item = groupingFilter.GroupItems[g].ElementAt(panel + 1);
return ReferenceEquals(Carousel.CurrentSelection, item.Model);
});
}
protected void WaitForSelection(int set, int? diff = null)
{
AddUntilStep($"selected is set{set}{(diff.HasValue ? $" diff{diff.Value}" : "")}", () =>
{
if (diff != null)
return ReferenceEquals(Carousel.CurrentSelection, BeatmapSets[set].Beatmaps[diff.Value]);
return BeatmapSets[set].Beatmaps.Contains(Carousel.CurrentSelection);
});
}
protected IEnumerable<T> GetVisiblePanels<T>()
where T : Drawable
{
return Carousel.ChildrenOfType<UserTrackingScrollContainer>().Single()
.ChildrenOfType<T>()
.Where(p => ((ICarouselPanel)p).Item?.IsVisible == true)
.OrderBy(p => p.Y);
}
protected void ClickVisiblePanel<T>(int index)
where T : Drawable
{
AddStep($"click panel at index {index}", () =>
{
Carousel.ChildrenOfType<UserTrackingScrollContainer>().Single()
.ChildrenOfType<T>()
.Where(p => ((ICarouselPanel)p).Item?.IsVisible == true)
.OrderBy(p => p.Y)
.ElementAt(index)
.ChildrenOfType<PanelBase>().Single()
.TriggerClick();
});
}
protected void ClickVisiblePanelWithOffset<T>(int index, Vector2 positionOffsetFromCentre)
where T : Drawable
{
AddStep($"move mouse to panel {index} with offset {positionOffsetFromCentre}", () =>
{
var panel = Carousel.ChildrenOfType<UserTrackingScrollContainer>().Single()
.ChildrenOfType<T>()
.Where(p => ((ICarouselPanel)p).Item?.IsVisible == true)
.OrderBy(p => p.Y)
.ElementAt(index);
InputManager.MoveMouseTo(panel.ScreenSpaceDrawQuad.Centre + panel.ToScreenSpace(positionOffsetFromCentre) - panel.ToScreenSpace(Vector2.Zero));
});
AddStep("click", () => InputManager.Click(MouseButton.Left));
}
/// <summary>
/// Add requested beatmap sets count to list.
/// </summary>
/// <param name="count">The count of beatmap sets to add.</param>
/// <param name="fixedDifficultiesPerSet">If not null, the number of difficulties per set. If null, randomised difficulty count will be used.</param>
/// <param name="randomMetadata">Whether to randomise the metadata to make groupings more uniform.</param>
protected void AddBeatmaps(int count, int? fixedDifficultiesPerSet = null, bool randomMetadata = false) => AddStep($"add {count} beatmaps{(randomMetadata ? " with random data" : "")}", () =>
{
for (int i = 0; i < count; i++)
BeatmapSets.Add(CreateTestBeatmapSetInfo(fixedDifficultiesPerSet, randomMetadata));
});
protected static BeatmapSetInfo CreateTestBeatmapSetInfo(int? fixedDifficultiesPerSet, bool randomMetadata)
{
var beatmapSetInfo = TestResources.CreateTestBeatmapSetInfo(fixedDifficultiesPerSet ?? RNG.Next(1, 4));
if (randomMetadata)
{
char randomCharacter = getRandomCharacter();
var metadata = new BeatmapMetadata
{
// Create random metadata, then we can check if sorting works based on these
Artist = $"{randomCharacter}ome Artist " + RNG.Next(0, 9),
Title = $"{randomCharacter}ome Song (set id {beatmapSetInfo.OnlineID:000}) {Guid.NewGuid()}",
Author = { Username = $"{randomCharacter}ome Guy " + RNG.Next(0, 9) },
};
foreach (var beatmap in beatmapSetInfo.Beatmaps)
beatmap.Metadata = metadata.DeepClone();
}
return beatmapSetInfo;
}
private static long randomCharPointer;
private static char getRandomCharacter()
{
const string chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz*";
return chars[(int)((randomCharPointer++ / 2) % chars.Length)];
}
protected void RemoveAllBeatmaps() => AddStep("clear all beatmaps", () => BeatmapSets.Clear());
protected void RemoveFirstBeatmap() =>
AddStep("remove first beatmap", () =>
{
if (BeatmapSets.Count == 0) return;
BeatmapSets.Remove(BeatmapSets.First());
});
private void updateStats()
{
if (Carousel.IsNull())
return;
stats.Clear();
createHeader("beatmap store");
stats.AddParagraph($"""
sets: {BeatmapSets.Count}
beatmaps: {beatmapCount}
""");
createHeader("carousel");
stats.AddParagraph($"""
sorting: {Carousel.IsFiltering}
tracked: {Carousel.ItemsTracked}
displayable: {Carousel.DisplayableItems}
displayed: {Carousel.VisibleItems}
selected: {Carousel.CurrentSelection}
""");
void createHeader(string text)
{
stats.AddParagraph(string.Empty);
stats.AddParagraph(text, cp =>
{
cp.Font = cp.Font.With(size: 18, weight: FontWeight.Bold);
});
}
}
}
}