mirror of
https://github.com/ppy/osu.git
synced 2026-06-04 06:23:37 +08:00
Merge branch 'master' into distance-snap-fix
This commit is contained in:
@@ -21,6 +21,7 @@ using osu.Game.Screens.SelectV2;
|
||||
using osu.Game.Tests.Beatmaps;
|
||||
using osu.Game.Tests.Resources;
|
||||
using osuTK.Graphics;
|
||||
using osuTK.Input;
|
||||
using BeatmapCarousel = osu.Game.Screens.SelectV2.BeatmapCarousel;
|
||||
|
||||
namespace osu.Game.Tests.Visual.SongSelect
|
||||
@@ -53,7 +54,7 @@ namespace osu.Game.Tests.Visual.SongSelect
|
||||
}
|
||||
|
||||
[SetUpSteps]
|
||||
public void SetUpSteps()
|
||||
public virtual void SetUpSteps()
|
||||
{
|
||||
RemoveAllBeatmaps();
|
||||
|
||||
@@ -129,12 +130,61 @@ namespace osu.Game.Tests.Visual.SongSelect
|
||||
});
|
||||
}
|
||||
|
||||
protected void SortBy(FilterCriteria criteria) => AddStep($"sort by {criteria.Sort}", () => Carousel.Filter(criteria));
|
||||
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 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 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)
|
||||
.TriggerClick();
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add requested beatmap sets count to list.
|
||||
/// </summary>
|
||||
|
||||
@@ -32,6 +32,13 @@ namespace osu.Game.Tests.Visual.SongSelect
|
||||
RemoveAllBeatmaps();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestOffScreenLoading()
|
||||
{
|
||||
AddStep("disable masking", () => Scroll.Masking = false);
|
||||
AddStep("enable masking", () => Scroll.Masking = true);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestAddRemoveOneByOne()
|
||||
{
|
||||
@@ -43,7 +50,7 @@ namespace osu.Game.Tests.Visual.SongSelect
|
||||
public void TestSorting()
|
||||
{
|
||||
AddBeatmaps(10);
|
||||
SortBy(new FilterCriteria { Sort = SortMode.Difficulty });
|
||||
SortBy(new FilterCriteria { Group = GroupMode.Difficulty, Sort = SortMode.Difficulty });
|
||||
SortBy(new FilterCriteria { Sort = SortMode.Artist });
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,158 @@
|
||||
// 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.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Screens.Select;
|
||||
using osu.Game.Screens.Select.Filter;
|
||||
using osu.Game.Screens.SelectV2;
|
||||
|
||||
namespace osu.Game.Tests.Visual.SongSelect
|
||||
{
|
||||
[TestFixture]
|
||||
public partial class TestSceneBeatmapCarouselV2GroupSelection : BeatmapCarouselV2TestScene
|
||||
{
|
||||
public override void SetUpSteps()
|
||||
{
|
||||
RemoveAllBeatmaps();
|
||||
|
||||
CreateCarousel();
|
||||
|
||||
SortBy(new FilterCriteria { Group = GroupMode.Difficulty, Sort = SortMode.Difficulty });
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestOpenCloseGroupWithNoSelectionMouse()
|
||||
{
|
||||
AddBeatmaps(10, 5);
|
||||
WaitForDrawablePanels();
|
||||
|
||||
AddAssert("no beatmaps visible", () => Carousel.ChildrenOfType<BeatmapPanel>().Count(p => p.Alpha > 0), () => Is.Zero);
|
||||
CheckNoSelection();
|
||||
|
||||
ClickVisiblePanel<GroupPanel>(0);
|
||||
AddUntilStep("some beatmaps visible", () => Carousel.ChildrenOfType<BeatmapPanel>().Count(p => p.Alpha > 0), () => Is.GreaterThan(0));
|
||||
CheckNoSelection();
|
||||
|
||||
ClickVisiblePanel<GroupPanel>(0);
|
||||
AddUntilStep("no beatmaps visible", () => Carousel.ChildrenOfType<BeatmapPanel>().Count(p => p.Alpha > 0), () => Is.Zero);
|
||||
CheckNoSelection();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestOpenCloseGroupWithNoSelectionKeyboard()
|
||||
{
|
||||
AddBeatmaps(10, 5);
|
||||
WaitForDrawablePanels();
|
||||
|
||||
AddAssert("no beatmaps visible", () => Carousel.ChildrenOfType<BeatmapPanel>().Count(p => p.Alpha > 0), () => Is.Zero);
|
||||
CheckNoSelection();
|
||||
|
||||
SelectNextPanel();
|
||||
Select();
|
||||
AddUntilStep("some beatmaps visible", () => Carousel.ChildrenOfType<BeatmapPanel>().Count(p => p.Alpha > 0), () => Is.GreaterThan(0));
|
||||
AddAssert("keyboard selected is expanded", () => getKeyboardSelectedPanel()?.Expanded.Value, () => Is.True);
|
||||
CheckNoSelection();
|
||||
|
||||
Select();
|
||||
AddUntilStep("no beatmaps visible", () => Carousel.ChildrenOfType<BeatmapPanel>().Count(p => p.Alpha > 0), () => Is.Zero);
|
||||
AddAssert("keyboard selected is collapsed", () => getKeyboardSelectedPanel()?.Expanded.Value, () => Is.False);
|
||||
CheckNoSelection();
|
||||
|
||||
GroupPanel? getKeyboardSelectedPanel() => Carousel.ChildrenOfType<GroupPanel>().SingleOrDefault(p => p.KeyboardSelected.Value);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestCarouselRemembersSelection()
|
||||
{
|
||||
AddBeatmaps(10);
|
||||
WaitForDrawablePanels();
|
||||
|
||||
SelectNextGroup();
|
||||
|
||||
object? selection = null;
|
||||
|
||||
AddStep("store drawable selection", () => selection = getSelectedPanel()?.Item?.Model);
|
||||
|
||||
CheckHasSelection();
|
||||
AddAssert("drawable selection non-null", () => selection, () => Is.Not.Null);
|
||||
AddAssert("drawable selection matches carousel selection", () => selection, () => Is.EqualTo(Carousel.CurrentSelection));
|
||||
|
||||
RemoveAllBeatmaps();
|
||||
AddUntilStep("no drawable selection", getSelectedPanel, () => Is.Null);
|
||||
|
||||
AddBeatmaps(10);
|
||||
WaitForDrawablePanels();
|
||||
|
||||
CheckHasSelection();
|
||||
AddAssert("no drawable selection", getSelectedPanel, () => Is.Null);
|
||||
|
||||
AddStep("add previous selection", () => BeatmapSets.Add(((BeatmapInfo)selection!).BeatmapSet!));
|
||||
|
||||
AddAssert("selection matches original carousel selection", () => selection, () => Is.EqualTo(Carousel.CurrentSelection));
|
||||
AddUntilStep("drawable selection restored", () => getSelectedPanel()?.Item?.Model, () => Is.EqualTo(selection));
|
||||
AddAssert("carousel item is visible", () => getSelectedPanel()?.Item?.IsVisible, () => Is.True);
|
||||
|
||||
ClickVisiblePanel<GroupPanel>(0);
|
||||
AddUntilStep("carousel item not visible", getSelectedPanel, () => Is.Null);
|
||||
|
||||
ClickVisiblePanel<GroupPanel>(0);
|
||||
AddUntilStep("carousel item is visible", () => getSelectedPanel()?.Item?.IsVisible, () => Is.True);
|
||||
|
||||
BeatmapPanel? getSelectedPanel() => Carousel.ChildrenOfType<BeatmapPanel>().SingleOrDefault(p => p.Selected.Value);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestGroupSelectionOnHeader()
|
||||
{
|
||||
AddBeatmaps(10, 3);
|
||||
WaitForDrawablePanels();
|
||||
|
||||
SelectNextGroup();
|
||||
WaitForGroupSelection(0, 0);
|
||||
|
||||
SelectPrevPanel();
|
||||
SelectPrevGroup();
|
||||
WaitForGroupSelection(2, 9);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestKeyboardSelection()
|
||||
{
|
||||
AddBeatmaps(10, 3);
|
||||
WaitForDrawablePanels();
|
||||
|
||||
SelectNextPanel();
|
||||
SelectNextPanel();
|
||||
SelectNextPanel();
|
||||
SelectNextPanel();
|
||||
CheckNoSelection();
|
||||
|
||||
// open first group
|
||||
Select();
|
||||
CheckNoSelection();
|
||||
AddUntilStep("some beatmaps visible", () => Carousel.ChildrenOfType<BeatmapPanel>().Count(p => p.Alpha > 0), () => Is.GreaterThan(0));
|
||||
|
||||
SelectNextPanel();
|
||||
Select();
|
||||
WaitForGroupSelection(0, 0);
|
||||
|
||||
SelectNextGroup();
|
||||
WaitForGroupSelection(0, 1);
|
||||
|
||||
SelectNextGroup();
|
||||
WaitForGroupSelection(0, 2);
|
||||
|
||||
SelectPrevGroup();
|
||||
WaitForGroupSelection(0, 1);
|
||||
|
||||
SelectPrevGroup();
|
||||
WaitForGroupSelection(0, 0);
|
||||
|
||||
SelectPrevGroup();
|
||||
WaitForGroupSelection(2, 9);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -22,10 +22,10 @@ namespace osu.Game.Tests.Visual.SongSelect
|
||||
{
|
||||
AddBeatmaps(10);
|
||||
WaitForDrawablePanels();
|
||||
checkNoSelection();
|
||||
CheckNoSelection();
|
||||
|
||||
select();
|
||||
checkNoSelection();
|
||||
Select();
|
||||
CheckNoSelection();
|
||||
|
||||
AddStep("press down arrow", () => InputManager.PressKey(Key.Down));
|
||||
checkSelectionIterating(false);
|
||||
@@ -39,8 +39,8 @@ namespace osu.Game.Tests.Visual.SongSelect
|
||||
AddStep("release up arrow", () => InputManager.ReleaseKey(Key.Up));
|
||||
checkSelectionIterating(false);
|
||||
|
||||
select();
|
||||
checkHasSelection();
|
||||
Select();
|
||||
CheckHasSelection();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -52,7 +52,7 @@ namespace osu.Game.Tests.Visual.SongSelect
|
||||
{
|
||||
AddBeatmaps(10);
|
||||
WaitForDrawablePanels();
|
||||
checkNoSelection();
|
||||
CheckNoSelection();
|
||||
|
||||
AddStep("press right arrow", () => InputManager.PressKey(Key.Right));
|
||||
checkSelectionIterating(true);
|
||||
@@ -73,13 +73,13 @@ namespace osu.Game.Tests.Visual.SongSelect
|
||||
AddBeatmaps(10);
|
||||
WaitForDrawablePanels();
|
||||
|
||||
selectNextGroup();
|
||||
SelectNextGroup();
|
||||
|
||||
object? selection = null;
|
||||
|
||||
AddStep("store drawable selection", () => selection = getSelectedPanel()?.Item?.Model);
|
||||
|
||||
checkHasSelection();
|
||||
CheckHasSelection();
|
||||
AddAssert("drawable selection non-null", () => selection, () => Is.Not.Null);
|
||||
AddAssert("drawable selection matches carousel selection", () => selection, () => Is.EqualTo(Carousel.CurrentSelection));
|
||||
|
||||
@@ -89,13 +89,14 @@ namespace osu.Game.Tests.Visual.SongSelect
|
||||
AddBeatmaps(10);
|
||||
WaitForDrawablePanels();
|
||||
|
||||
checkHasSelection();
|
||||
CheckHasSelection();
|
||||
AddAssert("no drawable selection", getSelectedPanel, () => Is.Null);
|
||||
|
||||
AddStep("add previous selection", () => BeatmapSets.Add(((BeatmapInfo)selection!).BeatmapSet!));
|
||||
|
||||
AddAssert("selection matches original carousel selection", () => selection, () => Is.EqualTo(Carousel.CurrentSelection));
|
||||
AddUntilStep("drawable selection restored", () => getSelectedPanel()?.Item?.Model, () => Is.EqualTo(selection));
|
||||
AddAssert("drawable selection matches carousel selection", () => selection, () => Is.EqualTo(Carousel.CurrentSelection));
|
||||
AddAssert("carousel item is visible", () => getSelectedPanel()?.Item?.IsVisible, () => Is.True);
|
||||
|
||||
BeatmapPanel? getSelectedPanel() => Carousel.ChildrenOfType<BeatmapPanel>().SingleOrDefault(p => p.Selected.Value);
|
||||
}
|
||||
@@ -108,10 +109,10 @@ namespace osu.Game.Tests.Visual.SongSelect
|
||||
AddBeatmaps(total_set_count);
|
||||
WaitForDrawablePanels();
|
||||
|
||||
selectNextGroup();
|
||||
waitForSelection(0, 0);
|
||||
selectPrevGroup();
|
||||
waitForSelection(total_set_count - 1, 0);
|
||||
SelectNextGroup();
|
||||
WaitForSelection(0, 0);
|
||||
SelectPrevGroup();
|
||||
WaitForSelection(total_set_count - 1, 0);
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -122,10 +123,25 @@ namespace osu.Game.Tests.Visual.SongSelect
|
||||
AddBeatmaps(total_set_count);
|
||||
WaitForDrawablePanels();
|
||||
|
||||
selectPrevGroup();
|
||||
waitForSelection(total_set_count - 1, 0);
|
||||
selectNextGroup();
|
||||
waitForSelection(0, 0);
|
||||
SelectPrevGroup();
|
||||
WaitForSelection(total_set_count - 1, 0);
|
||||
SelectNextGroup();
|
||||
WaitForSelection(0, 0);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestGroupSelectionOnHeader()
|
||||
{
|
||||
AddBeatmaps(10, 3);
|
||||
WaitForDrawablePanels();
|
||||
|
||||
SelectNextGroup();
|
||||
SelectNextGroup();
|
||||
WaitForSelection(1, 0);
|
||||
|
||||
SelectPrevPanel();
|
||||
SelectPrevGroup();
|
||||
WaitForSelection(0, 0);
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -134,71 +150,50 @@ namespace osu.Game.Tests.Visual.SongSelect
|
||||
AddBeatmaps(10, 3);
|
||||
WaitForDrawablePanels();
|
||||
|
||||
selectNextPanel();
|
||||
selectNextPanel();
|
||||
selectNextPanel();
|
||||
selectNextPanel();
|
||||
checkNoSelection();
|
||||
SelectNextPanel();
|
||||
SelectNextPanel();
|
||||
SelectNextPanel();
|
||||
SelectNextPanel();
|
||||
CheckNoSelection();
|
||||
|
||||
select();
|
||||
waitForSelection(3, 0);
|
||||
Select();
|
||||
WaitForSelection(3, 0);
|
||||
|
||||
selectNextPanel();
|
||||
waitForSelection(3, 0);
|
||||
SelectNextPanel();
|
||||
WaitForSelection(3, 0);
|
||||
|
||||
select();
|
||||
waitForSelection(3, 1);
|
||||
Select();
|
||||
WaitForSelection(3, 1);
|
||||
|
||||
selectNextPanel();
|
||||
waitForSelection(3, 1);
|
||||
SelectNextPanel();
|
||||
WaitForSelection(3, 1);
|
||||
|
||||
select();
|
||||
waitForSelection(3, 2);
|
||||
Select();
|
||||
WaitForSelection(3, 2);
|
||||
|
||||
selectNextPanel();
|
||||
waitForSelection(3, 2);
|
||||
SelectNextPanel();
|
||||
WaitForSelection(3, 2);
|
||||
|
||||
select();
|
||||
waitForSelection(4, 0);
|
||||
Select();
|
||||
WaitForSelection(4, 0);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestEmptyTraversal()
|
||||
{
|
||||
selectNextPanel();
|
||||
checkNoSelection();
|
||||
SelectNextPanel();
|
||||
CheckNoSelection();
|
||||
|
||||
selectNextGroup();
|
||||
checkNoSelection();
|
||||
SelectNextGroup();
|
||||
CheckNoSelection();
|
||||
|
||||
selectPrevPanel();
|
||||
checkNoSelection();
|
||||
SelectPrevPanel();
|
||||
CheckNoSelection();
|
||||
|
||||
selectPrevGroup();
|
||||
checkNoSelection();
|
||||
SelectPrevGroup();
|
||||
CheckNoSelection();
|
||||
}
|
||||
|
||||
private 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);
|
||||
});
|
||||
}
|
||||
|
||||
private void selectNextPanel() => AddStep("select next panel", () => InputManager.Key(Key.Down));
|
||||
private void selectPrevPanel() => AddStep("select prev panel", () => InputManager.Key(Key.Up));
|
||||
private void selectNextGroup() => AddStep("select next group", () => InputManager.Key(Key.Right));
|
||||
private void selectPrevGroup() => AddStep("select prev group", () => InputManager.Key(Key.Left));
|
||||
|
||||
private void select() => AddStep("select", () => InputManager.Key(Key.Enter));
|
||||
|
||||
private void checkNoSelection() => AddAssert("has no selection", () => Carousel.CurrentSelection, () => Is.Null);
|
||||
private void checkHasSelection() => AddAssert("has selection", () => Carousel.CurrentSelection, () => Is.Not.Null);
|
||||
|
||||
private void checkSelectionIterating(bool isIterating)
|
||||
{
|
||||
object? selection = null;
|
||||
|
||||
@@ -6,6 +6,7 @@ using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Cursor;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Graphics.Containers;
|
||||
using osu.Game.Graphics.Cursor;
|
||||
using osu.Game.Graphics.UserInterfaceV2;
|
||||
using osu.Game.Localisation;
|
||||
@@ -27,87 +28,102 @@ namespace osu.Game.Tests.Visual.UserInterface
|
||||
Child = new PopoverContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Child = new FillFlowContainer
|
||||
Child = new OsuScrollContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.Y,
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Width = 400,
|
||||
Direction = FillDirection.Vertical,
|
||||
Spacing = new Vector2(5),
|
||||
Padding = new MarginPadding(10),
|
||||
Children = new Drawable[]
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Child = new FillFlowContainer
|
||||
{
|
||||
new FormTextBox
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Width = 400,
|
||||
Direction = FillDirection.Vertical,
|
||||
Spacing = new Vector2(5),
|
||||
Padding = new MarginPadding(10),
|
||||
Children = new Drawable[]
|
||||
{
|
||||
Caption = "Artist",
|
||||
HintText = "Poot artist here!",
|
||||
PlaceholderText = "Here is an artist",
|
||||
TabbableContentContainer = this,
|
||||
},
|
||||
new FormTextBox
|
||||
{
|
||||
Caption = "Artist",
|
||||
HintText = "Poot artist here!",
|
||||
PlaceholderText = "Here is an artist",
|
||||
Current = { Disabled = true },
|
||||
TabbableContentContainer = this,
|
||||
},
|
||||
new FormNumberBox(allowDecimals: true)
|
||||
{
|
||||
Caption = "Number",
|
||||
HintText = "Insert your favourite number",
|
||||
PlaceholderText = "Mine is 42!",
|
||||
TabbableContentContainer = this,
|
||||
},
|
||||
new FormCheckBox
|
||||
{
|
||||
Caption = EditorSetupStrings.LetterboxDuringBreaks,
|
||||
HintText = EditorSetupStrings.LetterboxDuringBreaksDescription,
|
||||
},
|
||||
new FormCheckBox
|
||||
{
|
||||
Caption = EditorSetupStrings.LetterboxDuringBreaks,
|
||||
HintText = EditorSetupStrings.LetterboxDuringBreaksDescription,
|
||||
Current = { Disabled = true },
|
||||
},
|
||||
new FormSliderBar<float>
|
||||
{
|
||||
Caption = "Slider",
|
||||
Current = new BindableFloat
|
||||
new FormTextBox
|
||||
{
|
||||
MinValue = 0,
|
||||
MaxValue = 10,
|
||||
Value = 5,
|
||||
Precision = 0.1f,
|
||||
Caption = "Artist",
|
||||
HintText = "Poot artist here!",
|
||||
PlaceholderText = "Here is an artist",
|
||||
TabbableContentContainer = this,
|
||||
},
|
||||
TabbableContentContainer = this,
|
||||
},
|
||||
new FormEnumDropdown<CountdownType>
|
||||
{
|
||||
Caption = EditorSetupStrings.EnableCountdown,
|
||||
HintText = EditorSetupStrings.CountdownDescription,
|
||||
},
|
||||
new FormFileSelector
|
||||
{
|
||||
Caption = "File selector",
|
||||
PlaceholderText = "Select a file",
|
||||
},
|
||||
new FormBeatmapFileSelector(true)
|
||||
{
|
||||
Caption = "File selector with intermediate choice dialog",
|
||||
PlaceholderText = "Select a file",
|
||||
},
|
||||
new FormColourPalette
|
||||
{
|
||||
Caption = "Combo colours",
|
||||
Colours =
|
||||
new FormTextBox
|
||||
{
|
||||
Colour4.Red,
|
||||
Colour4.Green,
|
||||
Colour4.Blue,
|
||||
Colour4.Yellow,
|
||||
}
|
||||
Caption = "Artist",
|
||||
HintText = "Poot artist here!",
|
||||
PlaceholderText = "Here is an artist",
|
||||
Current = { Disabled = true },
|
||||
TabbableContentContainer = this,
|
||||
},
|
||||
new FormNumberBox(allowDecimals: true)
|
||||
{
|
||||
Caption = "Number",
|
||||
HintText = "Insert your favourite number",
|
||||
PlaceholderText = "Mine is 42!",
|
||||
TabbableContentContainer = this,
|
||||
},
|
||||
new FormCheckBox
|
||||
{
|
||||
Caption = EditorSetupStrings.LetterboxDuringBreaks,
|
||||
HintText = EditorSetupStrings.LetterboxDuringBreaksDescription,
|
||||
},
|
||||
new FormCheckBox
|
||||
{
|
||||
Caption = EditorSetupStrings.LetterboxDuringBreaks,
|
||||
HintText = EditorSetupStrings.LetterboxDuringBreaksDescription,
|
||||
Current = { Disabled = true },
|
||||
},
|
||||
new FormSliderBar<float>
|
||||
{
|
||||
Caption = "Slider",
|
||||
Current = new BindableFloat
|
||||
{
|
||||
MinValue = 0,
|
||||
MaxValue = 10,
|
||||
Value = 5,
|
||||
Precision = 0.1f,
|
||||
},
|
||||
TabbableContentContainer = this,
|
||||
},
|
||||
new FormEnumDropdown<CountdownType>
|
||||
{
|
||||
Caption = EditorSetupStrings.EnableCountdown,
|
||||
HintText = EditorSetupStrings.CountdownDescription,
|
||||
},
|
||||
new FormFileSelector
|
||||
{
|
||||
Caption = "File selector",
|
||||
PlaceholderText = "Select a file",
|
||||
},
|
||||
new FormBeatmapFileSelector(true)
|
||||
{
|
||||
Caption = "File selector with intermediate choice dialog",
|
||||
PlaceholderText = "Select a file",
|
||||
},
|
||||
new FormColourPalette
|
||||
{
|
||||
Caption = "Combo colours",
|
||||
Colours =
|
||||
{
|
||||
Colour4.Red,
|
||||
Colour4.Green,
|
||||
Colour4.Blue,
|
||||
Colour4.Yellow,
|
||||
}
|
||||
},
|
||||
new FormButton
|
||||
{
|
||||
Caption = "No text in button",
|
||||
Action = () => { },
|
||||
},
|
||||
new FormButton
|
||||
{
|
||||
Caption = "Text in button which is pretty long and is very likely to wrap",
|
||||
ButtonText = "Foo the bar",
|
||||
Action = () => { },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -447,60 +447,31 @@ namespace osu.Game.Beatmaps.Formats
|
||||
|
||||
private void addPathData(TextWriter writer, IHasPath pathData, Vector2 position)
|
||||
{
|
||||
PathType? lastType = null;
|
||||
|
||||
for (int i = 0; i < pathData.Path.ControlPoints.Count; i++)
|
||||
{
|
||||
PathControlPoint point = pathData.Path.ControlPoints[i];
|
||||
|
||||
// Note that lazer's encoding format supports specifying multiple curve types for a slider path, which is not supported by stable.
|
||||
// Backwards compatibility with stable is handled by `LegacyBeatmapExporter` and `BezierConverter.ConvertToModernBezier()`.
|
||||
if (point.Type != null)
|
||||
{
|
||||
// We've reached a new (explicit) segment!
|
||||
|
||||
// Explicit segments have a new format in which the type is injected into the middle of the control point string.
|
||||
// To preserve compatibility with osu-stable as much as possible, explicit segments with the same type are converted to use implicit segments by duplicating the control point.
|
||||
// One exception are consecutive perfect curves, which aren't supported in osu!stable and can lead to decoding issues if encoded as implicit segments
|
||||
bool needsExplicitSegment = point.Type != lastType || point.Type == PathType.PERFECT_CURVE || i == pathData.Path.ControlPoints.Count - 1;
|
||||
|
||||
// Another exception to this is when the last two control points of the last segment were duplicated. This is not a scenario supported by osu!stable.
|
||||
// Lazer does not add implicit segments for the last two control points of _any_ explicit segment, so an explicit segment is forced in order to maintain consistency with the decoder.
|
||||
if (i > 1)
|
||||
switch (point.Type?.Type)
|
||||
{
|
||||
// We need to use the absolute control point position to determine equality, otherwise floating point issues may arise.
|
||||
Vector2 p1 = position + pathData.Path.ControlPoints[i - 1].Position;
|
||||
Vector2 p2 = position + pathData.Path.ControlPoints[i - 2].Position;
|
||||
case SplineType.BSpline:
|
||||
writer.Write(point.Type.Value.Degree > 0 ? $"B{point.Type.Value.Degree}|" : "B|");
|
||||
break;
|
||||
|
||||
if ((int)p1.X == (int)p2.X && (int)p1.Y == (int)p2.Y)
|
||||
needsExplicitSegment = true;
|
||||
}
|
||||
case SplineType.Catmull:
|
||||
writer.Write("C|");
|
||||
break;
|
||||
|
||||
if (needsExplicitSegment)
|
||||
{
|
||||
switch (point.Type?.Type)
|
||||
{
|
||||
case SplineType.BSpline:
|
||||
writer.Write(point.Type.Value.Degree > 0 ? $"B{point.Type.Value.Degree}|" : "B|");
|
||||
break;
|
||||
case SplineType.PerfectCurve:
|
||||
writer.Write("P|");
|
||||
break;
|
||||
|
||||
case SplineType.Catmull:
|
||||
writer.Write("C|");
|
||||
break;
|
||||
|
||||
case SplineType.PerfectCurve:
|
||||
writer.Write("P|");
|
||||
break;
|
||||
|
||||
case SplineType.Linear:
|
||||
writer.Write("L|");
|
||||
break;
|
||||
}
|
||||
|
||||
lastType = point.Type;
|
||||
}
|
||||
else
|
||||
{
|
||||
// New segment with the same type - duplicate the control point
|
||||
writer.Write(FormattableString.Invariant($"{position.X + point.Position.X}:{position.Y + point.Position.Y}|"));
|
||||
case SplineType.Linear:
|
||||
writer.Write("L|");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -120,18 +120,30 @@ namespace osu.Game.Database
|
||||
if (BezierConverter.CountSegments(hasPath.Path.ControlPoints) <= 1
|
||||
&& hasPath.Path.ControlPoints[0].Type!.Value.Degree == null) continue;
|
||||
|
||||
var newControlPoints = BezierConverter.ConvertToModernBezier(hasPath.Path.ControlPoints);
|
||||
|
||||
// Truncate control points to integer positions
|
||||
foreach (var pathControlPoint in newControlPoints)
|
||||
{
|
||||
pathControlPoint.Position = new Vector2(
|
||||
(float)Math.Floor(pathControlPoint.Position.X),
|
||||
(float)Math.Floor(pathControlPoint.Position.Y));
|
||||
}
|
||||
var convertedToBezier = BezierConverter.ConvertToModernBezier(hasPath.Path.ControlPoints);
|
||||
|
||||
hasPath.Path.ControlPoints.Clear();
|
||||
hasPath.Path.ControlPoints.AddRange(newControlPoints);
|
||||
|
||||
for (int i = 0; i < convertedToBezier.Count; i++)
|
||||
{
|
||||
var convertedPoint = convertedToBezier[i];
|
||||
|
||||
// Truncate control points to integer positions
|
||||
var position = new Vector2(
|
||||
(float)Math.Floor(convertedPoint.Position.X),
|
||||
(float)Math.Floor(convertedPoint.Position.Y));
|
||||
|
||||
// stable only supports a single curve type specification per slider.
|
||||
// we exploit the fact that the converted-to-Bézier path only has Bézier segments,
|
||||
// and thus we specify the Bézier curve type once ever at the start of the slider.
|
||||
hasPath.Path.ControlPoints.Add(new PathControlPoint(position, i == 0 ? PathType.BEZIER : null));
|
||||
|
||||
// however, the Bézier path as output by the converter has multiple segments.
|
||||
// `LegacyBeatmapEncoder` will attempt to encode this by emitting per-control-point curve type specs which don't do anything for stable.
|
||||
// instead, stable expects control points that start a segment to be present in the path twice in succession.
|
||||
if (convertedPoint.Type == PathType.BEZIER && i > 0)
|
||||
hasPath.Path.ControlPoints.Add(new PathControlPoint(position));
|
||||
}
|
||||
}
|
||||
|
||||
// Encode to legacy format
|
||||
|
||||
@@ -0,0 +1,189 @@
|
||||
// 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.Diagnostics;
|
||||
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.Graphics.Sprites;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Graphics.Backgrounds;
|
||||
using osu.Game.Graphics.Containers;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Overlays;
|
||||
using osuTK;
|
||||
using osuTK.Graphics;
|
||||
|
||||
namespace osu.Game.Graphics.UserInterfaceV2
|
||||
{
|
||||
public partial class FormButton : CompositeDrawable
|
||||
{
|
||||
/// <summary>
|
||||
/// Caption describing this button, displayed on the left of it.
|
||||
/// </summary>
|
||||
public LocalisableString Caption { get; init; }
|
||||
|
||||
public LocalisableString ButtonText { get; init; }
|
||||
|
||||
public Action? Action { get; init; }
|
||||
|
||||
[Resolved]
|
||||
private OverlayColourProvider colourProvider { get; set; } = null!;
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
RelativeSizeAxes = Axes.X;
|
||||
Height = 50;
|
||||
|
||||
Masking = true;
|
||||
CornerRadius = 5;
|
||||
CornerExponent = 2.5f;
|
||||
|
||||
InternalChildren = new Drawable[]
|
||||
{
|
||||
new Box
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Colour = colourProvider.Background5,
|
||||
},
|
||||
new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Padding = new MarginPadding
|
||||
{
|
||||
Left = 9,
|
||||
Right = 5,
|
||||
Vertical = 5,
|
||||
},
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new OsuTextFlowContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Width = 0.45f,
|
||||
Anchor = Anchor.CentreLeft,
|
||||
Origin = Anchor.CentreLeft,
|
||||
Text = Caption,
|
||||
},
|
||||
new Button
|
||||
{
|
||||
Action = Action,
|
||||
Text = ButtonText,
|
||||
RelativeSizeAxes = ButtonText == default ? Axes.None : Axes.X,
|
||||
Width = ButtonText == default ? 90 : 0.45f,
|
||||
Anchor = Anchor.CentreRight,
|
||||
Origin = Anchor.CentreRight,
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
protected override bool OnHover(HoverEvent e)
|
||||
{
|
||||
updateState();
|
||||
return true;
|
||||
}
|
||||
|
||||
protected override void OnHoverLost(HoverLostEvent e)
|
||||
{
|
||||
base.OnHoverLost(e);
|
||||
updateState();
|
||||
}
|
||||
|
||||
private void updateState()
|
||||
{
|
||||
BorderThickness = IsHovered ? 2 : 0;
|
||||
|
||||
if (IsHovered)
|
||||
BorderColour = colourProvider.Light4;
|
||||
}
|
||||
|
||||
public partial class Button : OsuButton
|
||||
{
|
||||
private TrianglesV2? triangles { get; set; }
|
||||
|
||||
protected override float HoverLayerFinalAlpha => 0;
|
||||
|
||||
private Color4? triangleGradientSecondColour;
|
||||
|
||||
public override Color4 BackgroundColour
|
||||
{
|
||||
get => base.BackgroundColour;
|
||||
set
|
||||
{
|
||||
base.BackgroundColour = value;
|
||||
triangleGradientSecondColour = BackgroundColour.Lighten(0.2f);
|
||||
updateColours();
|
||||
}
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OverlayColourProvider overlayColourProvider)
|
||||
{
|
||||
DefaultBackgroundColour = overlayColourProvider.Colour3;
|
||||
triangleGradientSecondColour ??= overlayColourProvider.Colour1;
|
||||
|
||||
if (Text == default)
|
||||
{
|
||||
Add(new SpriteIcon
|
||||
{
|
||||
Icon = FontAwesome.Solid.ChevronRight,
|
||||
Size = new Vector2(16),
|
||||
Shadow = true,
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
Content.CornerRadius = 2;
|
||||
|
||||
Add(triangles = new TrianglesV2
|
||||
{
|
||||
Thickness = 0.02f,
|
||||
SpawnRatio = 0.6f,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Depth = float.MaxValue,
|
||||
});
|
||||
|
||||
updateColours();
|
||||
}
|
||||
|
||||
private void updateColours()
|
||||
{
|
||||
if (triangles == null)
|
||||
return;
|
||||
|
||||
Debug.Assert(triangleGradientSecondColour != null);
|
||||
|
||||
triangles.Colour = ColourInfo.GradientVertical(triangleGradientSecondColour.Value, BackgroundColour);
|
||||
}
|
||||
|
||||
protected override bool OnHover(HoverEvent e)
|
||||
{
|
||||
Debug.Assert(triangleGradientSecondColour != null);
|
||||
|
||||
Background.FadeColour(triangleGradientSecondColour.Value, 300, Easing.OutQuint);
|
||||
return base.OnHover(e);
|
||||
}
|
||||
|
||||
protected override void OnHoverLost(HoverLostEvent e)
|
||||
{
|
||||
Background.FadeColour(BackgroundColour, 300, Easing.OutQuint);
|
||||
base.OnHoverLost(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -92,35 +92,113 @@ namespace osu.Game.Screens.SelectV2
|
||||
|
||||
#region Selection handling
|
||||
|
||||
protected override void HandleItemSelected(object? model)
|
||||
private GroupDefinition? lastSelectedGroup;
|
||||
private BeatmapInfo? lastSelectedBeatmap;
|
||||
|
||||
protected override bool HandleItemSelected(object? model)
|
||||
{
|
||||
base.HandleItemSelected(model);
|
||||
|
||||
// Selecting a set isn't valid – let's re-select the first difficulty.
|
||||
if (model is BeatmapSetInfo setInfo)
|
||||
switch (model)
|
||||
{
|
||||
CurrentSelection = setInfo.Beatmaps.First();
|
||||
return;
|
||||
case GroupDefinition group:
|
||||
// Special case – collapsing an open group.
|
||||
if (lastSelectedGroup == group)
|
||||
{
|
||||
setExpansionStateOfGroup(lastSelectedGroup, false);
|
||||
lastSelectedGroup = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
setExpandedGroup(group);
|
||||
return false;
|
||||
|
||||
case BeatmapSetInfo setInfo:
|
||||
// Selecting a set isn't valid – let's re-select the first difficulty.
|
||||
CurrentSelection = setInfo.Beatmaps.First();
|
||||
return false;
|
||||
|
||||
case BeatmapInfo beatmapInfo:
|
||||
|
||||
// If we have groups, we need to account for them.
|
||||
if (Criteria.SplitOutDifficulties)
|
||||
{
|
||||
// Find the containing group. There should never be too many groups so iterating is efficient enough.
|
||||
GroupDefinition? group = grouping.GroupItems.SingleOrDefault(kvp => kvp.Value.Any(i => ReferenceEquals(i.Model, beatmapInfo))).Key;
|
||||
|
||||
if (group != null)
|
||||
setExpandedGroup(group);
|
||||
}
|
||||
else
|
||||
{
|
||||
setExpandedSet(beatmapInfo);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
if (model is BeatmapInfo beatmapInfo)
|
||||
setVisibilityOfSetItems(beatmapInfo.BeatmapSet!, true);
|
||||
return true;
|
||||
}
|
||||
|
||||
protected override void HandleItemDeselected(object? model)
|
||||
protected override bool CheckValidForGroupSelection(CarouselItem item)
|
||||
{
|
||||
base.HandleItemDeselected(model);
|
||||
|
||||
if (model is BeatmapInfo beatmapInfo)
|
||||
setVisibilityOfSetItems(beatmapInfo.BeatmapSet!, false);
|
||||
}
|
||||
|
||||
private void setVisibilityOfSetItems(BeatmapSetInfo set, bool visible)
|
||||
{
|
||||
if (grouping.SetItems.TryGetValue(set, out var group))
|
||||
switch (item.Model)
|
||||
{
|
||||
foreach (var i in group)
|
||||
i.IsVisible = visible;
|
||||
case BeatmapSetInfo:
|
||||
return true;
|
||||
|
||||
case BeatmapInfo:
|
||||
return Criteria.SplitOutDifficulties;
|
||||
|
||||
case GroupDefinition:
|
||||
return false;
|
||||
|
||||
default:
|
||||
throw new ArgumentException($"Unsupported model type {item.Model}");
|
||||
}
|
||||
}
|
||||
|
||||
private void setExpandedGroup(GroupDefinition group)
|
||||
{
|
||||
if (lastSelectedGroup != null)
|
||||
setExpansionStateOfGroup(lastSelectedGroup, false);
|
||||
lastSelectedGroup = group;
|
||||
setExpansionStateOfGroup(group, true);
|
||||
}
|
||||
|
||||
private void setExpansionStateOfGroup(GroupDefinition group, bool expanded)
|
||||
{
|
||||
if (grouping.GroupItems.TryGetValue(group, out var items))
|
||||
{
|
||||
foreach (var i in items)
|
||||
{
|
||||
if (i.Model is GroupDefinition)
|
||||
i.IsExpanded = expanded;
|
||||
else
|
||||
i.IsVisible = expanded;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void setExpandedSet(BeatmapInfo beatmapInfo)
|
||||
{
|
||||
if (lastSelectedBeatmap != null)
|
||||
setExpansionStateOfSetItems(lastSelectedBeatmap.BeatmapSet!, false);
|
||||
lastSelectedBeatmap = beatmapInfo;
|
||||
setExpansionStateOfSetItems(beatmapInfo.BeatmapSet!, true);
|
||||
}
|
||||
|
||||
private void setExpansionStateOfSetItems(BeatmapSetInfo set, bool expanded)
|
||||
{
|
||||
if (grouping.SetItems.TryGetValue(set, out var items))
|
||||
{
|
||||
foreach (var i in items)
|
||||
{
|
||||
if (i.Model is BeatmapSetInfo)
|
||||
i.IsExpanded = expanded;
|
||||
else
|
||||
i.IsVisible = expanded;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -143,9 +221,11 @@ namespace osu.Game.Screens.SelectV2
|
||||
|
||||
private readonly DrawablePool<BeatmapPanel> beatmapPanelPool = new DrawablePool<BeatmapPanel>(100);
|
||||
private readonly DrawablePool<BeatmapSetPanel> setPanelPool = new DrawablePool<BeatmapSetPanel>(100);
|
||||
private readonly DrawablePool<GroupPanel> groupPanelPool = new DrawablePool<GroupPanel>(100);
|
||||
|
||||
private void setupPools()
|
||||
{
|
||||
AddInternal(groupPanelPool);
|
||||
AddInternal(beatmapPanelPool);
|
||||
AddInternal(setPanelPool);
|
||||
}
|
||||
@@ -154,7 +234,12 @@ namespace osu.Game.Screens.SelectV2
|
||||
{
|
||||
switch (item.Model)
|
||||
{
|
||||
case GroupDefinition:
|
||||
return groupPanelPool.Get();
|
||||
|
||||
case BeatmapInfo:
|
||||
// TODO: if beatmap is a group selection target, it needs to be a different drawable
|
||||
// with more information attached.
|
||||
return beatmapPanelPool.Get();
|
||||
|
||||
case BeatmapSetInfo:
|
||||
@@ -166,4 +251,6 @@ namespace osu.Game.Screens.SelectV2
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
public record GroupDefinition(string Title);
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Screens.Select;
|
||||
using osu.Game.Screens.Select.Filter;
|
||||
|
||||
namespace osu.Game.Screens.SelectV2
|
||||
{
|
||||
@@ -18,7 +19,13 @@ namespace osu.Game.Screens.SelectV2
|
||||
/// </summary>
|
||||
public IDictionary<BeatmapSetInfo, HashSet<CarouselItem>> SetItems => setItems;
|
||||
|
||||
/// <summary>
|
||||
/// Groups contain children which are group-selectable. This dictionary holds the relationships between groups-panels to allow expanding them on selection.
|
||||
/// </summary>
|
||||
public IDictionary<GroupDefinition, HashSet<CarouselItem>> GroupItems => groupItems;
|
||||
|
||||
private readonly Dictionary<BeatmapSetInfo, HashSet<CarouselItem>> setItems = new Dictionary<BeatmapSetInfo, HashSet<CarouselItem>>();
|
||||
private readonly Dictionary<GroupDefinition, HashSet<CarouselItem>> groupItems = new Dictionary<GroupDefinition, HashSet<CarouselItem>>();
|
||||
|
||||
private readonly Func<FilterCriteria> getCriteria;
|
||||
|
||||
@@ -29,50 +36,97 @@ namespace osu.Game.Screens.SelectV2
|
||||
|
||||
public async Task<IEnumerable<CarouselItem>> Run(IEnumerable<CarouselItem> items, CancellationToken cancellationToken) => await Task.Run(() =>
|
||||
{
|
||||
bool groupSetsTogether;
|
||||
|
||||
setItems.Clear();
|
||||
groupItems.Clear();
|
||||
|
||||
var criteria = getCriteria();
|
||||
|
||||
if (criteria.SplitOutDifficulties)
|
||||
{
|
||||
foreach (var item in items)
|
||||
{
|
||||
item.IsVisible = true;
|
||||
item.IsGroupSelectionTarget = true;
|
||||
}
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
CarouselItem? lastItem = null;
|
||||
|
||||
var newItems = new List<CarouselItem>(items.Count());
|
||||
|
||||
foreach (var item in items)
|
||||
// Add criteria groups.
|
||||
switch (criteria.Group)
|
||||
{
|
||||
default:
|
||||
groupSetsTogether = true;
|
||||
newItems.AddRange(items);
|
||||
break;
|
||||
|
||||
case GroupMode.Difficulty:
|
||||
groupSetsTogether = false;
|
||||
int starGroup = int.MinValue;
|
||||
|
||||
foreach (var item in items)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
var b = (BeatmapInfo)item.Model;
|
||||
|
||||
if (b.StarRating > starGroup)
|
||||
{
|
||||
starGroup = (int)Math.Floor(b.StarRating);
|
||||
var groupDefinition = new GroupDefinition($"{starGroup} - {++starGroup} *");
|
||||
var groupItem = new CarouselItem(groupDefinition) { DrawHeight = GroupPanel.HEIGHT };
|
||||
|
||||
newItems.Add(groupItem);
|
||||
groupItems[groupDefinition] = new HashSet<CarouselItem> { groupItem };
|
||||
}
|
||||
|
||||
newItems.Add(item);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
// Add set headers wherever required.
|
||||
CarouselItem? lastItem = null;
|
||||
|
||||
if (groupSetsTogether)
|
||||
{
|
||||
for (int i = 0; i < newItems.Count; i++)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
var item = newItems[i];
|
||||
|
||||
if (item.Model is BeatmapInfo beatmap)
|
||||
{
|
||||
bool newBeatmapSet = lastItem == null || (lastItem.Model is BeatmapInfo lastBeatmap && lastBeatmap.BeatmapSet!.ID != beatmap.BeatmapSet!.ID);
|
||||
|
||||
if (newBeatmapSet)
|
||||
{
|
||||
var setItem = new CarouselItem(beatmap.BeatmapSet!) { DrawHeight = BeatmapSetPanel.HEIGHT };
|
||||
setItems[beatmap.BeatmapSet!] = new HashSet<CarouselItem> { setItem };
|
||||
newItems.Insert(i, setItem);
|
||||
i++;
|
||||
}
|
||||
|
||||
setItems[beatmap.BeatmapSet!].Add(item);
|
||||
item.IsVisible = false;
|
||||
}
|
||||
|
||||
lastItem = item;
|
||||
}
|
||||
}
|
||||
|
||||
// Link group items to their headers.
|
||||
GroupDefinition? lastGroup = null;
|
||||
|
||||
foreach (var item in newItems)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
if (item.Model is BeatmapInfo b)
|
||||
if (item.Model is GroupDefinition group)
|
||||
{
|
||||
// Add set header
|
||||
if (lastItem == null || (lastItem.Model is BeatmapInfo b2 && b2.BeatmapSet!.OnlineID != b.BeatmapSet!.OnlineID))
|
||||
{
|
||||
newItems.Add(new CarouselItem(b.BeatmapSet!)
|
||||
{
|
||||
DrawHeight = BeatmapSetPanel.HEIGHT,
|
||||
IsGroupSelectionTarget = true
|
||||
});
|
||||
}
|
||||
|
||||
if (!setItems.TryGetValue(b.BeatmapSet!, out var related))
|
||||
setItems[b.BeatmapSet!] = related = new HashSet<CarouselItem>();
|
||||
|
||||
related.Add(item);
|
||||
lastGroup = group;
|
||||
continue;
|
||||
}
|
||||
|
||||
newItems.Add(item);
|
||||
lastItem = item;
|
||||
|
||||
item.IsGroupSelectionTarget = false;
|
||||
item.IsVisible = false;
|
||||
if (lastGroup != null)
|
||||
{
|
||||
groupItems[lastGroup].Add(item);
|
||||
item.IsVisible = false;
|
||||
}
|
||||
}
|
||||
|
||||
return newItems;
|
||||
|
||||
@@ -100,6 +100,7 @@ namespace osu.Game.Screens.SelectV2
|
||||
|
||||
public CarouselItem? Item { get; set; }
|
||||
public BindableBool Selected { get; } = new BindableBool();
|
||||
public BindableBool Expanded { get; } = new BindableBool();
|
||||
public BindableBool KeyboardSelected { get; } = new BindableBool();
|
||||
|
||||
public double DrawYPosition { get; set; }
|
||||
|
||||
@@ -25,6 +25,7 @@ namespace osu.Game.Screens.SelectV2
|
||||
private BeatmapCarousel carousel { get; set; } = null!;
|
||||
|
||||
private OsuSpriteText text = null!;
|
||||
private Box box = null!;
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
@@ -34,7 +35,7 @@ namespace osu.Game.Screens.SelectV2
|
||||
|
||||
InternalChildren = new Drawable[]
|
||||
{
|
||||
new Box
|
||||
box = new Box
|
||||
{
|
||||
Colour = Color4.Yellow.Darken(5),
|
||||
Alpha = 0.8f,
|
||||
@@ -48,6 +49,11 @@ namespace osu.Game.Screens.SelectV2
|
||||
}
|
||||
};
|
||||
|
||||
Expanded.BindValueChanged(value =>
|
||||
{
|
||||
box.FadeColour(value.NewValue ? Color4.Yellow.Darken(2) : Color4.Yellow.Darken(5), 500, Easing.OutQuint);
|
||||
});
|
||||
|
||||
KeyboardSelected.BindValueChanged(value =>
|
||||
{
|
||||
if (value.NewValue)
|
||||
@@ -67,7 +73,6 @@ namespace osu.Game.Screens.SelectV2
|
||||
base.PrepareForUse();
|
||||
|
||||
Debug.Assert(Item != null);
|
||||
Debug.Assert(Item.IsGroupSelectionTarget);
|
||||
|
||||
var beatmapSetInfo = (BeatmapSetInfo)Item.Model;
|
||||
|
||||
@@ -86,6 +91,7 @@ namespace osu.Game.Screens.SelectV2
|
||||
|
||||
public CarouselItem? Item { get; set; }
|
||||
public BindableBool Selected { get; } = new BindableBool();
|
||||
public BindableBool Expanded { get; } = new BindableBool();
|
||||
public BindableBool KeyboardSelected { get; } = new BindableBool();
|
||||
|
||||
public double DrawYPosition { get; set; }
|
||||
|
||||
@@ -8,6 +8,7 @@ using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Caching;
|
||||
using osu.Framework.Extensions.TypeExtensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
@@ -130,7 +131,7 @@ namespace osu.Game.Screens.SelectV2
|
||||
///
|
||||
/// A filter may add, mutate or remove items.
|
||||
/// </remarks>
|
||||
protected IEnumerable<ICarouselFilter> Filters { get; init; } = Enumerable.Empty<ICarouselFilter>();
|
||||
public IEnumerable<ICarouselFilter> Filters { get; init; } = Enumerable.Empty<ICarouselFilter>();
|
||||
|
||||
/// <summary>
|
||||
/// All items which are to be considered for display in this carousel.
|
||||
@@ -167,12 +168,18 @@ namespace osu.Game.Screens.SelectV2
|
||||
protected Drawable? GetMaterialisedDrawableForItem(CarouselItem item) =>
|
||||
scroll.Panels.SingleOrDefault(p => ((ICarouselPanel)p).Item == item);
|
||||
|
||||
/// <summary>
|
||||
/// When a user is traversing the carousel via group selection keys, assert whether the item provided is a valid target.
|
||||
/// </summary>
|
||||
/// <param name="item">The candidate item.</param>
|
||||
/// <returns>Whether the provided item is a valid group target. If <c>false</c>, more panels will be checked in the user's requested direction until a valid target is found.</returns>
|
||||
protected virtual bool CheckValidForGroupSelection(CarouselItem item) => true;
|
||||
|
||||
/// <summary>
|
||||
/// Called when an item is "selected".
|
||||
/// </summary>
|
||||
protected virtual void HandleItemSelected(object? model)
|
||||
{
|
||||
}
|
||||
/// <returns>Whether the item should be selected.</returns>
|
||||
protected virtual bool HandleItemSelected(object? model) => true;
|
||||
|
||||
/// <summary>
|
||||
/// Called when an item is "deselected".
|
||||
@@ -205,7 +212,6 @@ namespace osu.Game.Screens.SelectV2
|
||||
InternalChild = scroll = new CarouselScrollContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Masking = false,
|
||||
};
|
||||
|
||||
Items.BindCollectionChanged((_, _) => FilterAsync());
|
||||
@@ -303,19 +309,19 @@ namespace osu.Game.Screens.SelectV2
|
||||
return true;
|
||||
|
||||
case GlobalAction.SelectNext:
|
||||
selectNext(1, isGroupSelection: false);
|
||||
return true;
|
||||
|
||||
case GlobalAction.SelectNextGroup:
|
||||
selectNext(1, isGroupSelection: true);
|
||||
traverseKeyboardSelection(1);
|
||||
return true;
|
||||
|
||||
case GlobalAction.SelectPrevious:
|
||||
selectNext(-1, isGroupSelection: false);
|
||||
traverseKeyboardSelection(-1);
|
||||
return true;
|
||||
|
||||
case GlobalAction.SelectNextGroup:
|
||||
traverseGroupSelection(1);
|
||||
return true;
|
||||
|
||||
case GlobalAction.SelectPreviousGroup:
|
||||
selectNext(-1, isGroupSelection: true);
|
||||
traverseGroupSelection(-1);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -326,91 +332,99 @@ namespace osu.Game.Screens.SelectV2
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Select the next valid selection relative to a current selection.
|
||||
/// This is generally for keyboard based traversal.
|
||||
/// </summary>
|
||||
/// <param name="direction">Positive for downwards, negative for upwards.</param>
|
||||
/// <param name="isGroupSelection">Whether the selection should traverse groups. Group selection updates the actual selection immediately, while non-group selection will only prepare a future keyboard selection.</param>
|
||||
/// <returns>Whether selection was possible.</returns>
|
||||
private bool selectNext(int direction, bool isGroupSelection)
|
||||
private void traverseKeyboardSelection(int direction)
|
||||
{
|
||||
// Ensure sanity
|
||||
Debug.Assert(direction != 0);
|
||||
direction = direction > 0 ? 1 : -1;
|
||||
if (carouselItems == null || carouselItems.Count == 0) return;
|
||||
|
||||
if (carouselItems == null || carouselItems.Count == 0)
|
||||
return false;
|
||||
int originalIndex;
|
||||
|
||||
// If the user has a different keyboard selection and requests
|
||||
// group selection, first transfer the keyboard selection to actual selection.
|
||||
if (isGroupSelection && currentSelection.CarouselItem != currentKeyboardSelection.CarouselItem)
|
||||
{
|
||||
TryActivateSelection();
|
||||
return true;
|
||||
}
|
||||
if (currentKeyboardSelection.Index != null)
|
||||
originalIndex = currentKeyboardSelection.Index.Value;
|
||||
else if (direction > 0)
|
||||
originalIndex = carouselItems.Count - 1;
|
||||
else
|
||||
originalIndex = 0;
|
||||
|
||||
CarouselItem? selectionItem = currentKeyboardSelection.CarouselItem;
|
||||
int selectionIndex = currentKeyboardSelection.Index ?? -1;
|
||||
|
||||
// To keep things simple, let's first handle the cases where there's no selection yet.
|
||||
if (selectionItem == null || selectionIndex < 0)
|
||||
{
|
||||
// Start by selecting the first item.
|
||||
selectionItem = carouselItems.First();
|
||||
selectionIndex = 0;
|
||||
|
||||
// In the forwards case, immediately attempt selection of this panel.
|
||||
// If selection fails, continue with standard logic to find the next valid selection.
|
||||
if (direction > 0 && attemptSelection(selectionItem))
|
||||
return true;
|
||||
|
||||
// In the backwards direction we can just allow the selection logic to go ahead and loop around to the last valid.
|
||||
}
|
||||
|
||||
Debug.Assert(selectionItem != null);
|
||||
|
||||
// As a second special case, if we're group selecting backwards and the current selection isn't a group,
|
||||
// make sure to go back to the group header this item belongs to, so that the block below doesn't find it and stop too early.
|
||||
if (isGroupSelection && direction < 0)
|
||||
{
|
||||
while (!carouselItems[selectionIndex].IsGroupSelectionTarget)
|
||||
selectionIndex--;
|
||||
}
|
||||
|
||||
CarouselItem? newItem;
|
||||
int newIndex = originalIndex;
|
||||
|
||||
// Iterate over every item back to the current selection, finding the first valid item.
|
||||
// The fail condition is when we reach the selection after a cyclic loop over every item.
|
||||
do
|
||||
{
|
||||
selectionIndex += direction;
|
||||
newItem = carouselItems[(selectionIndex + carouselItems.Count) % carouselItems.Count];
|
||||
newIndex = (newIndex + direction + carouselItems.Count) % carouselItems.Count;
|
||||
var newItem = carouselItems[newIndex];
|
||||
|
||||
if (attemptSelection(newItem))
|
||||
return true;
|
||||
} while (newItem != selectionItem);
|
||||
if (newItem.IsVisible)
|
||||
{
|
||||
setKeyboardSelection(newItem.Model);
|
||||
return;
|
||||
}
|
||||
} while (newIndex != originalIndex);
|
||||
}
|
||||
|
||||
return false;
|
||||
/// <summary>
|
||||
/// Select the next valid selection relative to a current selection.
|
||||
/// This is generally for keyboard based traversal.
|
||||
/// </summary>
|
||||
/// <param name="direction">Positive for downwards, negative for upwards.</param>
|
||||
/// <returns>Whether selection was possible.</returns>
|
||||
private void traverseGroupSelection(int direction)
|
||||
{
|
||||
if (carouselItems == null || carouselItems.Count == 0) return;
|
||||
|
||||
bool attemptSelection(CarouselItem item)
|
||||
// If the user has a different keyboard selection and requests
|
||||
// group selection, first transfer the keyboard selection to actual selection.
|
||||
if (currentSelection.CarouselItem != currentKeyboardSelection.CarouselItem)
|
||||
{
|
||||
if (!item.IsVisible || (isGroupSelection && !item.IsGroupSelectionTarget))
|
||||
return false;
|
||||
TryActivateSelection();
|
||||
|
||||
if (isGroupSelection)
|
||||
setSelection(item.Model);
|
||||
else
|
||||
setKeyboardSelection(item.Model);
|
||||
|
||||
return true;
|
||||
// There's a chance this couldn't resolve, at which point continue with standard traversal.
|
||||
if (currentSelection.CarouselItem == currentKeyboardSelection.CarouselItem)
|
||||
return;
|
||||
}
|
||||
|
||||
int originalIndex;
|
||||
int newIndex;
|
||||
|
||||
if (currentSelection.Index == null)
|
||||
{
|
||||
// If there's no current selection, start from either end of the full list.
|
||||
newIndex = originalIndex = direction > 0 ? carouselItems.Count - 1 : 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
newIndex = originalIndex = currentSelection.Index.Value;
|
||||
|
||||
// As a second special case, if we're group selecting backwards and the current selection isn't a group,
|
||||
// make sure to go back to the group header this item belongs to, so that the block below doesn't find it and stop too early.
|
||||
if (direction < 0)
|
||||
{
|
||||
while (!CheckValidForGroupSelection(carouselItems[newIndex]))
|
||||
newIndex--;
|
||||
}
|
||||
}
|
||||
|
||||
// Iterate over every item back to the current selection, finding the first valid item.
|
||||
// The fail condition is when we reach the selection after a cyclic loop over every item.
|
||||
do
|
||||
{
|
||||
newIndex = (newIndex + direction + carouselItems.Count) % carouselItems.Count;
|
||||
var newItem = carouselItems[newIndex];
|
||||
|
||||
if (CheckValidForGroupSelection(newItem))
|
||||
{
|
||||
setSelection(newItem.Model);
|
||||
return;
|
||||
}
|
||||
} while (newIndex != originalIndex);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Selection handling
|
||||
|
||||
private readonly Cached selectionValid = new Cached();
|
||||
|
||||
private Selection currentKeyboardSelection = new Selection();
|
||||
private Selection currentSelection = new Selection();
|
||||
|
||||
@@ -419,29 +433,22 @@ namespace osu.Game.Screens.SelectV2
|
||||
if (currentSelection.Model == model)
|
||||
return;
|
||||
|
||||
var previousSelection = currentSelection;
|
||||
if (HandleItemSelected(model))
|
||||
{
|
||||
if (currentSelection.Model != null)
|
||||
HandleItemDeselected(currentSelection.Model);
|
||||
|
||||
if (previousSelection.Model != null)
|
||||
HandleItemDeselected(previousSelection.Model);
|
||||
currentKeyboardSelection = new Selection(model);
|
||||
currentSelection = currentKeyboardSelection;
|
||||
}
|
||||
|
||||
currentSelection = currentKeyboardSelection = new Selection(model);
|
||||
HandleItemSelected(currentSelection.Model);
|
||||
|
||||
// `HandleItemSelected` can alter `CurrentSelection`, which will recursively call `setSelection()` again.
|
||||
// if that happens, the rest of this method should be a no-op.
|
||||
if (currentSelection.Model != model)
|
||||
return;
|
||||
|
||||
refreshAfterSelection();
|
||||
scrollToSelection();
|
||||
selectionValid.Invalidate();
|
||||
}
|
||||
|
||||
private void setKeyboardSelection(object? model)
|
||||
{
|
||||
currentKeyboardSelection = new Selection(model);
|
||||
|
||||
refreshAfterSelection();
|
||||
scrollToSelection();
|
||||
selectionValid.Invalidate();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -526,6 +533,13 @@ namespace osu.Game.Screens.SelectV2
|
||||
if (carouselItems == null)
|
||||
return;
|
||||
|
||||
if (!selectionValid.IsValid)
|
||||
{
|
||||
refreshAfterSelection();
|
||||
scrollToSelection();
|
||||
selectionValid.Validate();
|
||||
}
|
||||
|
||||
var range = getDisplayRange();
|
||||
|
||||
if (range != displayedRange)
|
||||
@@ -544,8 +558,8 @@ namespace osu.Game.Screens.SelectV2
|
||||
if (c.Item == null)
|
||||
continue;
|
||||
|
||||
if (panel.Depth != c.DrawYPosition)
|
||||
scroll.Panels.ChangeChildDepth(panel, (float)c.DrawYPosition);
|
||||
double selectedYPos = currentSelection?.CarouselItem?.CarouselYPosition ?? 0;
|
||||
scroll.Panels.ChangeChildDepth(panel, (float)Math.Abs(c.DrawYPosition - selectedYPos));
|
||||
|
||||
if (c.DrawYPosition != c.Item.CarouselYPosition)
|
||||
c.DrawYPosition = Interpolation.DampContinuously(c.DrawYPosition, c.Item.CarouselYPosition, 50, Time.Elapsed);
|
||||
@@ -557,6 +571,7 @@ namespace osu.Game.Screens.SelectV2
|
||||
|
||||
c.Selected.Value = c.Item == currentSelection?.CarouselItem;
|
||||
c.KeyboardSelected.Value = c.Item == currentKeyboardSelection?.CarouselItem;
|
||||
c.Expanded.Value = c.Item.IsExpanded;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -660,6 +675,7 @@ namespace osu.Game.Screens.SelectV2
|
||||
carouselPanel.Item = null;
|
||||
carouselPanel.Selected.Value = false;
|
||||
carouselPanel.KeyboardSelected.Value = false;
|
||||
carouselPanel.Expanded.Value = false;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
@@ -30,15 +30,15 @@ namespace osu.Game.Screens.SelectV2
|
||||
public float DrawHeight { get; set; } = DEFAULT_HEIGHT;
|
||||
|
||||
/// <summary>
|
||||
/// Whether this item should be a valid target for user group selection hotkeys.
|
||||
/// </summary>
|
||||
public bool IsGroupSelectionTarget { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether this item is visible or collapsed (hidden).
|
||||
/// Whether this item is visible or hidden.
|
||||
/// </summary>
|
||||
public bool IsVisible { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Whether this item is expanded or not. Should only be used for headers of groups.
|
||||
/// </summary>
|
||||
public bool IsExpanded { get; set; }
|
||||
|
||||
public CarouselItem(object model)
|
||||
{
|
||||
Model = model;
|
||||
|
||||
@@ -0,0 +1,120 @@
|
||||
// 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.Diagnostics;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Extensions.Color4Extensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Pooling;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osuTK;
|
||||
using osuTK.Graphics;
|
||||
|
||||
namespace osu.Game.Screens.SelectV2
|
||||
{
|
||||
public partial class GroupPanel : PoolableDrawable, ICarouselPanel
|
||||
{
|
||||
public const float HEIGHT = CarouselItem.DEFAULT_HEIGHT * 2;
|
||||
|
||||
[Resolved]
|
||||
private BeatmapCarousel carousel { get; set; } = null!;
|
||||
|
||||
private Box activationFlash = null!;
|
||||
private OsuSpriteText text = null!;
|
||||
|
||||
private Box box = null!;
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
Size = new Vector2(500, HEIGHT);
|
||||
Masking = true;
|
||||
|
||||
InternalChildren = new Drawable[]
|
||||
{
|
||||
box = new Box
|
||||
{
|
||||
Colour = Color4.DarkBlue.Darken(5),
|
||||
Alpha = 0.8f,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
},
|
||||
activationFlash = new Box
|
||||
{
|
||||
Colour = Color4.White,
|
||||
Blending = BlendingParameters.Additive,
|
||||
Alpha = 0,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
},
|
||||
text = new OsuSpriteText
|
||||
{
|
||||
Padding = new MarginPadding(5),
|
||||
Anchor = Anchor.CentreLeft,
|
||||
Origin = Anchor.CentreLeft,
|
||||
}
|
||||
};
|
||||
|
||||
Selected.BindValueChanged(value =>
|
||||
{
|
||||
activationFlash.FadeTo(value.NewValue ? 0.2f : 0, 500, Easing.OutQuint);
|
||||
});
|
||||
|
||||
Expanded.BindValueChanged(value =>
|
||||
{
|
||||
box.FadeColour(value.NewValue ? Color4.SkyBlue : Color4.DarkBlue.Darken(5), 500, Easing.OutQuint);
|
||||
});
|
||||
|
||||
KeyboardSelected.BindValueChanged(value =>
|
||||
{
|
||||
if (value.NewValue)
|
||||
{
|
||||
BorderThickness = 5;
|
||||
BorderColour = Color4.Pink;
|
||||
}
|
||||
else
|
||||
{
|
||||
BorderThickness = 0;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected override void PrepareForUse()
|
||||
{
|
||||
base.PrepareForUse();
|
||||
|
||||
Debug.Assert(Item != null);
|
||||
|
||||
GroupDefinition group = (GroupDefinition)Item.Model;
|
||||
|
||||
text.Text = group.Title;
|
||||
|
||||
this.FadeInFromZero(500, Easing.OutQuint);
|
||||
}
|
||||
|
||||
protected override bool OnClick(ClickEvent e)
|
||||
{
|
||||
carousel.CurrentSelection = Item!.Model;
|
||||
return true;
|
||||
}
|
||||
|
||||
#region ICarouselPanel
|
||||
|
||||
public CarouselItem? Item { get; set; }
|
||||
public BindableBool Selected { get; } = new BindableBool();
|
||||
public BindableBool Expanded { get; } = new BindableBool();
|
||||
public BindableBool KeyboardSelected { get; } = new BindableBool();
|
||||
|
||||
public double DrawYPosition { get; set; }
|
||||
|
||||
public void Activated()
|
||||
{
|
||||
// sets should never be activated.
|
||||
throw new InvalidOperationException();
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -14,10 +14,15 @@ namespace osu.Game.Screens.SelectV2
|
||||
public interface ICarouselPanel
|
||||
{
|
||||
/// <summary>
|
||||
/// Whether this item has selection. Should be read from to update the visual state.
|
||||
/// Whether this item has selection (see <see cref="Carousel{T}.CurrentSelection"/>). Should be read from to update the visual state.
|
||||
/// </summary>
|
||||
BindableBool Selected { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether this item is expanded (see <see cref="CarouselItem.IsExpanded"/>). Should be read from to update the visual state.
|
||||
/// </summary>
|
||||
BindableBool Expanded { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether this item has keyboard selection. Should be read from to update the visual state.
|
||||
/// </summary>
|
||||
|
||||
Reference in New Issue
Block a user