Migrate mod select overlay footer content

This commit is contained in:
Salman Ahmed 2024-06-29 08:38:43 +03:00
parent 467d7c4f54
commit 48bf3f1385
7 changed files with 257 additions and 170 deletions

View File

@ -24,6 +24,7 @@ using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Rulesets.Taiko.Mods;
using osu.Game.Screens.Footer;
using osu.Game.Tests.Mods;
using osuTK;
using osuTK.Input;
@ -93,12 +94,28 @@ namespace osu.Game.Tests.Visual.UserInterface
private void createScreen()
{
AddStep("create screen", () => Child = modSelectOverlay = new TestModSelectOverlay
AddStep("create screen", () =>
{
RelativeSizeAxes = Axes.Both,
State = { Value = Visibility.Visible },
Beatmap = Beatmap.Value,
SelectedMods = { BindTarget = SelectedMods }
var receptor = new ScreenFooter.BackReceptor();
var footer = new ScreenFooter(receptor);
Child = new DependencyProvidingContainer
{
RelativeSizeAxes = Axes.Both,
CachedDependencies = new[] { (typeof(ScreenFooter), (object)footer) },
Children = new Drawable[]
{
receptor,
modSelectOverlay = new TestModSelectOverlay
{
RelativeSizeAxes = Axes.Both,
State = { Value = Visibility.Visible },
Beatmap = { Value = Beatmap.Value },
SelectedMods = { BindTarget = SelectedMods },
},
footer,
}
};
});
waitForColumnLoad();
}
@ -119,7 +136,7 @@ namespace osu.Game.Tests.Visual.UserInterface
AddAssert("mod multiplier correct", () =>
{
double multiplier = SelectedMods.Value.Aggregate(1d, (m, mod) => m * mod.ScoreMultiplier);
return Precision.AlmostEquals(multiplier, modSelectOverlay.ChildrenOfType<RankingInformationDisplay>().Single().ModMultiplier.Value);
return Precision.AlmostEquals(multiplier, this.ChildrenOfType<RankingInformationDisplay>().Single().ModMultiplier.Value);
});
assertCustomisationToggleState(disabled: false, active: false);
AddAssert("setting items created", () => modSelectOverlay.ChildrenOfType<ISettingsItem>().Any());
@ -134,7 +151,7 @@ namespace osu.Game.Tests.Visual.UserInterface
AddAssert("mod multiplier correct", () =>
{
double multiplier = SelectedMods.Value.Aggregate(1d, (m, mod) => m * mod.ScoreMultiplier);
return Precision.AlmostEquals(multiplier, modSelectOverlay.ChildrenOfType<RankingInformationDisplay>().Single().ModMultiplier.Value);
return Precision.AlmostEquals(multiplier, this.ChildrenOfType<RankingInformationDisplay>().Single().ModMultiplier.Value);
});
assertCustomisationToggleState(disabled: false, active: false);
AddAssert("setting items created", () => modSelectOverlay.ChildrenOfType<ISettingsItem>().Any());
@ -756,7 +773,7 @@ namespace osu.Game.Tests.Visual.UserInterface
AddStep("click back button", () =>
{
InputManager.MoveMouseTo(modSelectOverlay.BackButton);
InputManager.MoveMouseTo(this.ChildrenOfType<ScreenBackButton>().Single());
InputManager.Click(MouseButton.Left);
});
AddAssert("mod select hidden", () => modSelectOverlay.State.Value == Visibility.Hidden);
@ -884,7 +901,7 @@ namespace osu.Game.Tests.Visual.UserInterface
InputManager.Click(MouseButton.Left);
});
AddAssert("difficulty multiplier display shows correct value",
() => modSelectOverlay.ChildrenOfType<RankingInformationDisplay>().Single().ModMultiplier.Value, () => Is.EqualTo(0.1).Within(Precision.DOUBLE_EPSILON));
() => this.ChildrenOfType<RankingInformationDisplay>().Single().ModMultiplier.Value, () => Is.EqualTo(0.1).Within(Precision.DOUBLE_EPSILON));
// this is highly unorthodox in a test, but because the `ModSettingChangeTracker` machinery heavily leans on events and object disposal and re-creation,
// it is instrumental in the reproduction of the failure scenario that this test is supposed to cover.
@ -894,7 +911,7 @@ namespace osu.Game.Tests.Visual.UserInterface
AddStep("reset half time speed to default", () => modSelectOverlay.ChildrenOfType<ModCustomisationPanel>().Single()
.ChildrenOfType<RevertToDefaultButton<double>>().Single().TriggerClick());
AddUntilStep("difficulty multiplier display shows correct value",
() => modSelectOverlay.ChildrenOfType<RankingInformationDisplay>().Single().ModMultiplier.Value, () => Is.EqualTo(0.3).Within(Precision.DOUBLE_EPSILON));
() => this.ChildrenOfType<RankingInformationDisplay>().Single().ModMultiplier.Value, () => Is.EqualTo(0.3).Within(Precision.DOUBLE_EPSILON));
}
[Test]
@ -1014,8 +1031,6 @@ namespace osu.Game.Tests.Visual.UserInterface
private partial class TestModSelectOverlay : UserModSelectOverlay
{
protected override bool ShowPresets => true;
public new ShearedButton BackButton => base.BackButton;
}
private class TestUnimplementedMod : Mod

View File

@ -0,0 +1,177 @@
// 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.Collections.Generic;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Beatmaps;
using osu.Game.Configuration;
using osu.Game.Graphics.UserInterface;
using osu.Game.Rulesets.Mods;
using osuTK;
namespace osu.Game.Overlays.Mods
{
public partial class ModSelectFooterContent : VisibilityContainer
{
private readonly ModSelectOverlay overlay;
private RankingInformationDisplay? rankingInformationDisplay;
private BeatmapAttributesDisplay? beatmapAttributesDisplay;
private FillFlowContainer<ShearedButton> buttonFlow = null!;
private FillFlowContainer contentFlow = null!;
public DeselectAllModsButton? DeselectAllModsButton { get; set; }
public readonly IBindable<WorkingBeatmap?> Beatmap = new Bindable<WorkingBeatmap?>();
public readonly IBindable<IReadOnlyList<Mod>> ActiveMods = new Bindable<IReadOnlyList<Mod>>();
/// <summary>
/// Whether the effects (on score multiplier, on or beatmap difficulty) of the current selected set of mods should be shown.
/// </summary>
protected virtual bool ShowModEffects => true;
/// <summary>
/// Whether the ranking information and beatmap attributes displays are stacked vertically due to small space.
/// </summary>
public bool DisplaysStackedVertically { get; private set; }
public ModSelectFooterContent(ModSelectOverlay overlay)
{
this.overlay = overlay;
}
[BackgroundDependencyLoader]
private void load()
{
RelativeSizeAxes = Axes.Both;
InternalChild = buttonFlow = new FillFlowContainer<ShearedButton>
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Direction = FillDirection.Horizontal,
Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomLeft,
Padding = new MarginPadding { Horizontal = 20 },
Spacing = new Vector2(10),
ChildrenEnumerable = CreateButtons(),
};
if (ShowModEffects)
{
AddInternal(contentFlow = new FillFlowContainer
{
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Vertical,
Spacing = new Vector2(30, 10),
Anchor = Anchor.BottomRight,
Origin = Anchor.BottomRight,
Margin = new MarginPadding { Horizontal = 20 },
Children = new Drawable[]
{
rankingInformationDisplay = new RankingInformationDisplay
{
Anchor = Anchor.BottomRight,
Origin = Anchor.BottomRight
},
beatmapAttributesDisplay = new BeatmapAttributesDisplay
{
Anchor = Anchor.BottomRight,
Origin = Anchor.BottomRight,
BeatmapInfo = { Value = Beatmap.Value?.BeatmapInfo },
},
}
});
}
}
private ModSettingChangeTracker? modSettingChangeTracker;
protected override void LoadComplete()
{
base.LoadComplete();
Beatmap.BindValueChanged(b =>
{
if (beatmapAttributesDisplay != null)
beatmapAttributesDisplay.BeatmapInfo.Value = b.NewValue?.BeatmapInfo;
}, true);
ActiveMods.BindValueChanged(m =>
{
updateInformation();
modSettingChangeTracker?.Dispose();
// Importantly, use ActiveMods.Value here (and not the ValueChanged NewValue) as the latter can
// potentially be stale, due to complexities in the way change trackers work.
//
// See https://github.com/ppy/osu/pull/23284#issuecomment-1529056988
modSettingChangeTracker = new ModSettingChangeTracker(ActiveMods.Value);
modSettingChangeTracker.SettingChanged += _ => updateInformation();
}, true);
}
private void updateInformation()
{
if (rankingInformationDisplay != null)
{
double multiplier = 1.0;
foreach (var mod in ActiveMods.Value)
multiplier *= mod.ScoreMultiplier;
rankingInformationDisplay.ModMultiplier.Value = multiplier;
rankingInformationDisplay.Ranked.Value = ActiveMods.Value.All(m => m.Ranked);
}
if (beatmapAttributesDisplay != null)
beatmapAttributesDisplay.Mods.Value = ActiveMods.Value;
}
protected override void Update()
{
base.Update();
if (beatmapAttributesDisplay != null)
{
float rightEdgeOfLastButton = buttonFlow[^1].ScreenSpaceDrawQuad.TopRight.X;
// this is cheating a bit; the 640 value is hardcoded based on how wide the expanded panel _generally_ is.
// due to the transition applied, the raw screenspace quad of the panel cannot be used, as it will trigger an ugly feedback cycle of expanding and collapsing.
float projectedLeftEdgeOfExpandedBeatmapAttributesDisplay = buttonFlow.ToScreenSpace(buttonFlow.DrawSize - new Vector2(640, 0)).X;
DisplaysStackedVertically = rightEdgeOfLastButton > projectedLeftEdgeOfExpandedBeatmapAttributesDisplay;
// only update preview panel's collapsed state after we are fully visible, to ensure all the buttons are where we expect them to be.
if (Alpha == 1)
beatmapAttributesDisplay.Collapsed.Value = DisplaysStackedVertically;
contentFlow.LayoutDuration = 200;
contentFlow.LayoutEasing = Easing.OutQuint;
contentFlow.Direction = DisplaysStackedVertically ? FillDirection.Vertical : FillDirection.Horizontal;
}
}
protected virtual IEnumerable<ShearedButton> CreateButtons() => new[]
{
DeselectAllModsButton = new DeselectAllModsButton(overlay)
};
protected override void PopIn()
{
this.MoveToY(0, 400, Easing.OutQuint)
.FadeIn(400, Easing.OutQuint);
}
protected override void PopOut()
{
this.MoveToY(-20f, 200, Easing.OutQuint)
.FadeOut(200, Easing.OutQuint);
}
}
}

View File

@ -27,6 +27,7 @@ using osu.Game.Graphics.UserInterface;
using osu.Game.Input.Bindings;
using osu.Game.Localisation;
using osu.Game.Rulesets.Mods;
using osu.Game.Screens.Footer;
using osu.Game.Utils;
using osuTK;
using osuTK.Graphics;
@ -87,11 +88,6 @@ namespace osu.Game.Overlays.Mods
public ShearedSearchTextBox SearchTextBox { get; private set; } = null!;
/// <summary>
/// Whether the effects (on score multiplier, on or beatmap difficulty) of the current selected set of mods should be shown.
/// </summary>
protected virtual bool ShowModEffects => true;
/// <summary>
/// Whether per-mod customisation controls are visible.
/// </summary>
@ -108,11 +104,6 @@ namespace osu.Game.Overlays.Mods
protected virtual IReadOnlyList<Mod> ComputeActiveMods() => SelectedMods.Value;
protected virtual IEnumerable<ShearedButton> CreateFooterButtons()
{
yield return deselectAllModsButton = new DeselectAllModsButton(this);
}
private readonly Bindable<Dictionary<ModType, IReadOnlyList<Mod>>> globalAvailableMods = new Bindable<Dictionary<ModType, IReadOnlyList<Mod>>>();
public IEnumerable<ModState> AllAvailableMods => AvailableMods.Value.SelectMany(pair => pair.Value);
@ -121,34 +112,18 @@ namespace osu.Game.Overlays.Mods
private ColumnScrollContainer columnScroll = null!;
private ColumnFlowContainer columnFlow = null!;
private FillFlowContainer<ShearedButton> footerButtonFlow = null!;
private FillFlowContainer footerContentFlow = null!;
private DeselectAllModsButton deselectAllModsButton = null!;
private Container aboveColumnsContent = null!;
private RankingInformationDisplay? rankingInformationDisplay;
private BeatmapAttributesDisplay? beatmapAttributesDisplay;
private ModCustomisationPanel customisationPanel = null!;
protected ShearedButton BackButton { get; private set; } = null!;
protected SelectAllModsButton? SelectAllModsButton { get; set; }
protected virtual SelectAllModsButton? SelectAllModsButton => null;
private Sample? columnAppearSample;
private WorkingBeatmap? beatmap;
public readonly Bindable<WorkingBeatmap?> Beatmap = new Bindable<WorkingBeatmap?>();
public WorkingBeatmap? Beatmap
{
get => beatmap;
set
{
if (beatmap == value) return;
beatmap = value;
if (IsLoaded && beatmapAttributesDisplay != null)
beatmapAttributesDisplay.BeatmapInfo.Value = beatmap?.BeatmapInfo;
}
}
[Resolved]
private ScreenFooter? footer { get; set; }
protected ModSelectOverlay(OverlayColourScheme colourScheme = OverlayColourScheme.Green)
: base(colourScheme)
@ -226,59 +201,6 @@ namespace osu.Game.Overlays.Mods
}
});
FooterContent.Add(footerButtonFlow = new FillFlowContainer<ShearedButton>
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Direction = FillDirection.Horizontal,
Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomLeft,
Padding = new MarginPadding
{
Vertical = PADDING,
Horizontal = 70
},
Spacing = new Vector2(10),
ChildrenEnumerable = CreateFooterButtons().Prepend(BackButton = new ShearedButton(BUTTON_WIDTH)
{
Text = CommonStrings.Back,
Action = Hide,
DarkerColour = colours.Pink2,
LighterColour = colours.Pink1
})
});
if (ShowModEffects)
{
FooterContent.Add(footerContentFlow = new FillFlowContainer
{
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Vertical,
Spacing = new Vector2(30, 10),
Anchor = Anchor.BottomRight,
Origin = Anchor.BottomRight,
Margin = new MarginPadding
{
Vertical = PADDING,
Horizontal = 20
},
Children = new Drawable[]
{
rankingInformationDisplay = new RankingInformationDisplay
{
Anchor = Anchor.BottomRight,
Origin = Anchor.BottomRight
},
beatmapAttributesDisplay = new BeatmapAttributesDisplay
{
Anchor = Anchor.BottomRight,
Origin = Anchor.BottomRight,
BeatmapInfo = { Value = Beatmap?.BeatmapInfo },
},
}
});
}
globalAvailableMods.BindTo(game.AvailableMods);
textSearchStartsActive = configManager.GetBindable<bool>(OsuSetting.ModSelectTextSearchStartsActive);
@ -292,8 +214,6 @@ namespace osu.Game.Overlays.Mods
SearchTextBox.Current.Value = string.Empty;
}
private ModSettingChangeTracker? modSettingChangeTracker;
protected override void LoadComplete()
{
// this is called before base call so that the mod state is populated early, and the transition in `PopIn()` can play out properly.
@ -316,23 +236,6 @@ namespace osu.Game.Overlays.Mods
ActiveMods.Value = ComputeActiveMods();
}, true);
ActiveMods.BindValueChanged(_ =>
{
updateOverlayInformation();
modSettingChangeTracker?.Dispose();
if (AllowCustomisation)
{
// Importantly, use ActiveMods.Value here (and not the ValueChanged NewValue) as the latter can
// potentially be stale, due to complexities in the way change trackers work.
//
// See https://github.com/ppy/osu/pull/23284#issuecomment-1529056988
modSettingChangeTracker = new ModSettingChangeTracker(ActiveMods.Value);
modSettingChangeTracker.SettingChanged += _ => updateOverlayInformation();
}
}, true);
customisationPanel.Expanded.BindValueChanged(_ => updateCustomisationVisualState(), true);
SearchTextBox.Current.BindValueChanged(query =>
@ -350,6 +253,16 @@ namespace osu.Game.Overlays.Mods
});
}
private ModSelectFooterContent? currentFooterContent;
public override bool UseNewFooter => true;
public override Drawable CreateFooterContent() => currentFooterContent = new ModSelectFooterContent(this)
{
Beatmap = { BindTarget = Beatmap },
ActiveMods = { BindTarget = ActiveMods },
};
private static readonly LocalisableString input_search_placeholder = Resources.Localisation.Web.CommonStrings.InputSearch;
private static readonly LocalisableString tab_to_search_placeholder = ModSelectOverlayStrings.TabToSearch;
@ -358,26 +271,7 @@ namespace osu.Game.Overlays.Mods
base.Update();
SearchTextBox.PlaceholderText = SearchTextBox.HasFocus ? input_search_placeholder : tab_to_search_placeholder;
if (beatmapAttributesDisplay != null)
{
float rightEdgeOfLastButton = footerButtonFlow[^1].ScreenSpaceDrawQuad.TopRight.X;
// this is cheating a bit; the 640 value is hardcoded based on how wide the expanded panel _generally_ is.
// due to the transition applied, the raw screenspace quad of the panel cannot be used, as it will trigger an ugly feedback cycle of expanding and collapsing.
float projectedLeftEdgeOfExpandedBeatmapAttributesDisplay = footerButtonFlow.ToScreenSpace(footerButtonFlow.DrawSize - new Vector2(640, 0)).X;
bool screenIsntWideEnough = rightEdgeOfLastButton > projectedLeftEdgeOfExpandedBeatmapAttributesDisplay;
// only update preview panel's collapsed state after we are fully visible, to ensure all the buttons are where we expect them to be.
if (Alpha == 1)
beatmapAttributesDisplay.Collapsed.Value = screenIsntWideEnough;
footerContentFlow.LayoutDuration = 200;
footerContentFlow.LayoutEasing = Easing.OutQuint;
footerContentFlow.Direction = screenIsntWideEnough ? FillDirection.Vertical : FillDirection.Horizontal;
aboveColumnsContent.Padding = aboveColumnsContent.Padding with { Bottom = screenIsntWideEnough ? 70f : 15f };
}
aboveColumnsContent.Padding = aboveColumnsContent.Padding with { Bottom = currentFooterContent?.DisplaysStackedVertically == true ? 75f : 15f };
}
/// <summary>
@ -455,27 +349,6 @@ namespace osu.Game.Overlays.Mods
modState.ValidForSelection.Value = modState.Mod.Type != ModType.System && modState.Mod.HasImplementation && IsValidMod.Invoke(modState.Mod);
}
/// <summary>
/// Updates any information displayed on the overlay regarding the effects of the active mods.
/// This reads from <see cref="ActiveMods"/> instead of <see cref="SelectedMods"/>.
/// </summary>
private void updateOverlayInformation()
{
if (rankingInformationDisplay != null)
{
double multiplier = 1.0;
foreach (var mod in ActiveMods.Value)
multiplier *= mod.ScoreMultiplier;
rankingInformationDisplay.ModMultiplier.Value = multiplier;
rankingInformationDisplay.Ranked.Value = ActiveMods.Value.All(m => m.Ranked);
}
if (beatmapAttributesDisplay != null)
beatmapAttributesDisplay.Mods.Value = ActiveMods.Value;
}
private void updateCustomisation()
{
if (!AllowCustomisation)
@ -701,7 +574,7 @@ namespace osu.Game.Overlays.Mods
{
if (!SearchTextBox.HasFocus && !customisationPanel.Expanded.Value)
{
deselectAllModsButton.TriggerClick();
currentFooterContent?.DeselectAllModsButton?.TriggerClick();
return true;
}
@ -732,7 +605,7 @@ namespace osu.Game.Overlays.Mods
return base.OnPressed(e);
void hideOverlay() => BackButton.TriggerClick();
void hideOverlay() => footer?.BackButton.TriggerClick();
}
/// <inheritdoc cref="IKeyBindingHandler{PlatformAction}"/>
@ -740,7 +613,7 @@ namespace osu.Game.Overlays.Mods
/// This is handled locally here due to conflicts in input handling between the search text box and the select all mods button.
/// Attempting to handle this action locally in both places leads to a possible scenario
/// wherein activating the "select all" platform binding will both select all text in the search box and select all mods.
/// </remarks>>
/// </remarks>
public bool OnPressed(KeyBindingPressEvent<PlatformAction> e)
{
if (e.Repeat || e.Action != PlatformAction.SelectAll || SelectAllModsButton == null)

View File

@ -14,8 +14,6 @@ namespace osu.Game.Screens.OnlinePlay
{
public partial class FreeModSelectOverlay : ModSelectOverlay
{
protected override bool ShowModEffects => false;
protected override bool AllowCustomisation => false;
public new Func<Mod, bool> IsValidMod
@ -24,6 +22,10 @@ namespace osu.Game.Screens.OnlinePlay
set => base.IsValidMod = m => m.UserPlayable && value.Invoke(m);
}
private FreeModSelectFooterContent? currentFooterContent;
protected override SelectAllModsButton? SelectAllModsButton => currentFooterContent?.SelectAllModsButton;
public FreeModSelectOverlay()
: base(OverlayColourScheme.Plum)
{
@ -32,12 +34,33 @@ namespace osu.Game.Screens.OnlinePlay
protected override ModColumn CreateModColumn(ModType modType) => new ModColumn(modType, true);
protected override IEnumerable<ShearedButton> CreateFooterButtons()
=> base.CreateFooterButtons()
.Prepend(SelectAllModsButton = new SelectAllModsButton(this)
{
Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomLeft,
});
public override Drawable CreateFooterContent() => currentFooterContent = new FreeModSelectFooterContent(this)
{
Beatmap = { BindTarget = Beatmap },
ActiveMods = { BindTarget = ActiveMods },
};
private partial class FreeModSelectFooterContent : ModSelectFooterContent
{
private readonly FreeModSelectOverlay overlay;
protected override bool ShowModEffects => false;
public SelectAllModsButton? SelectAllModsButton;
public FreeModSelectFooterContent(FreeModSelectOverlay overlay)
: base(overlay)
{
this.overlay = overlay;
}
protected override IEnumerable<ShearedButton> CreateButtons()
=> base.CreateButtons()
.Prepend(SelectAllModsButton = new SelectAllModsButton(overlay)
{
Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomLeft,
});
}
}
}

View File

@ -453,7 +453,7 @@ namespace osu.Game.Screens.OnlinePlay.Match
// Retrieve the corresponding local beatmap, since we can't directly use the playlist's beatmap info
var localBeatmap = beatmap == null ? null : beatmapManager.QueryBeatmap(b => b.OnlineID == beatmap.OnlineID);
UserModsSelectOverlay.Beatmap = Beatmap.Value = beatmapManager.GetWorkingBeatmap(localBeatmap);
UserModsSelectOverlay.Beatmap.Value = Beatmap.Value = beatmapManager.GetWorkingBeatmap(localBeatmap);
}
protected virtual void UpdateMods()

View File

@ -851,7 +851,7 @@ namespace osu.Game.Screens.Select
BeatmapDetails.Beatmap = beatmap;
ModSelect.Beatmap = beatmap;
ModSelect.Beatmap.Value = beatmap;
advancedStats.BeatmapInfo = beatmap.BeatmapInfo;

View File

@ -658,7 +658,6 @@ namespace osu.Game.Tests.Visual.Gameplay
private partial class TestModSelectOverlay : UserModSelectOverlay
{
protected override bool ShowModEffects => true;
protected override bool ShowPresets => false;
public TestModSelectOverlay()