1
0
mirror of https://github.com/ppy/osu.git synced 2025-02-14 17:45:20 +08:00

Merge branch 'master' into sorted-breaks

This commit is contained in:
Bartłomiej Dach 2024-07-08 15:59:32 +02:00
commit 877b5768fc
No known key found for this signature in database
34 changed files with 1111 additions and 489 deletions

View File

@ -1,10 +1,13 @@
// 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.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Testing;
using osu.Game.Graphics.UserInterface;
using osu.Game.Rulesets.Osu;
using osu.Game.Screens.Edit;
using osu.Game.Screens.Edit.Components;
@ -19,9 +22,10 @@ namespace osu.Game.Tests.Visual.Editing
[Cached]
private EditorBeatmap editorBeatmap = new EditorBeatmap(new TestBeatmap(new OsuRuleset().RulesetInfo));
public TestSceneEditorClock()
[SetUpSteps]
public void SetUpSteps()
{
Add(new FillFlowContainer
AddStep("create content", () => Add(new FillFlowContainer
{
RelativeSizeAxes = Axes.Both,
Children = new Drawable[]
@ -39,19 +43,17 @@ namespace osu.Game.Tests.Visual.Editing
Size = new Vector2(200, 100)
}
}
}));
AddStep("set working beatmap", () =>
{
Beatmap.Disabled = false;
Beatmap.Value = CreateWorkingBeatmap(new OsuRuleset().RulesetInfo);
// ensure that music controller does not change this beatmap due to it
// completing naturally as part of the test.
Beatmap.Disabled = true;
});
}
protected override void LoadComplete()
{
base.LoadComplete();
Beatmap.Value = CreateWorkingBeatmap(new OsuRuleset().RulesetInfo);
// ensure that music controller does not change this beatmap due to it
// completing naturally as part of the test.
Beatmap.Disabled = true;
}
[Test]
public void TestStopAtTrackEnd()
{
@ -102,6 +104,29 @@ namespace osu.Game.Tests.Visual.Editing
AddUntilStep("time is clamped to track length", () => EditorClock.CurrentTime, () => Is.EqualTo(EditorClock.TrackLength));
}
[Test]
public void TestCurrentTimeDoubleTransform()
{
AddAssert("seek smoothly twice and current time is accurate", () =>
{
EditorClock.SeekSmoothlyTo(1000);
EditorClock.SeekSmoothlyTo(2000);
return 2000 == EditorClock.CurrentTimeAccurate;
});
}
[Test]
public void TestAdjustmentsRemovedOnDisposal()
{
AddStep("reset clock", () => EditorClock.Seek(0));
AddStep("set 0.25x speed", () => this.ChildrenOfType<OsuTabControl<double>>().First().Current.Value = 0.25);
AddAssert("track has 0.25x tempo", () => Beatmap.Value.Track.AggregateTempo.Value, () => Is.EqualTo(0.25));
AddStep("dispose playback control", () => Clear(disposeChildren: true));
AddAssert("track has 1x tempo", () => Beatmap.Value.Track.AggregateTempo.Value, () => Is.EqualTo(1));
}
protected override void Dispose(bool isDisposing)
{
Beatmap.Disabled = false;

View File

@ -0,0 +1,180 @@
// 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 NUnit.Framework;
using osu.Game.Audio.Effects;
using osu.Game.Overlays;
namespace osu.Game.Tests.Visual.Menus
{
public partial class TestSceneAudioDucking : OsuGameTestScene
{
[Test]
public void TestMomentaryDuck()
{
AddStep("duck momentarily", () => Game.MusicController.DuckMomentarily(1000));
}
[Test]
public void TestMultipleDucks()
{
IDisposable duckOp1 = null!;
IDisposable duckOp2 = null!;
double normalVolume = 1;
AddStep("get initial volume", () =>
{
normalVolume = Game.Audio.Tracks.AggregateVolume.Value;
});
AddStep("duck one", () =>
{
duckOp1 = Game.MusicController.Duck(new DuckParameters
{
DuckVolumeTo = 0.5,
});
});
AddUntilStep("wait for duck to complete", () => Game.Audio.Tracks.AggregateVolume.Value, () => Is.EqualTo(normalVolume * 0.5f).Within(0.01));
AddStep("duck two", () =>
{
duckOp2 = Game.MusicController.Duck(new DuckParameters
{
DuckVolumeTo = 0.2,
});
});
AddUntilStep("wait for duck to complete", () => Game.Audio.Tracks.AggregateVolume.Value, () => Is.EqualTo(normalVolume * 0.2f).Within(0.01));
AddStep("restore two", () => duckOp2.Dispose());
AddUntilStep("wait for restore to complete", () => Game.Audio.Tracks.AggregateVolume.Value, () => Is.EqualTo(normalVolume * 0.5f).Within(0.01));
AddStep("restore one", () => duckOp1.Dispose());
AddUntilStep("wait for restore to complete", () => Game.Audio.Tracks.AggregateVolume.Value, () => Is.EqualTo(normalVolume).Within(0.01));
}
[Test]
public void TestMultipleDucksSameParameters()
{
var duckParameters = new DuckParameters
{
DuckVolumeTo = 0.5,
};
IDisposable duckOp1 = null!;
IDisposable duckOp2 = null!;
double normalVolume = 1;
AddStep("get initial volume", () =>
{
normalVolume = Game.Audio.Tracks.AggregateVolume.Value;
});
AddStep("duck one", () =>
{
duckOp1 = Game.MusicController.Duck(duckParameters);
});
AddUntilStep("wait for duck to complete", () => Game.Audio.Tracks.AggregateVolume.Value, () => Is.EqualTo(normalVolume * 0.5f).Within(0.01));
AddStep("duck two", () =>
{
duckOp2 = Game.MusicController.Duck(duckParameters);
});
AddUntilStep("wait for duck to complete", () => Game.Audio.Tracks.AggregateVolume.Value, () => Is.EqualTo(normalVolume * 0.5f).Within(0.01));
AddStep("restore two", () => duckOp2.Dispose());
AddUntilStep("wait for restore to complete", () => Game.Audio.Tracks.AggregateVolume.Value, () => Is.EqualTo(normalVolume * 0.5f).Within(0.01));
AddStep("restore one", () => duckOp1.Dispose());
AddUntilStep("wait for restore to complete", () => Game.Audio.Tracks.AggregateVolume.Value, () => Is.EqualTo(normalVolume).Within(0.01));
}
[Test]
public void TestMultipleDucksReverseOrder()
{
IDisposable duckOp1 = null!;
IDisposable duckOp2 = null!;
double normalVolume = 1;
AddStep("get initial volume", () =>
{
normalVolume = Game.Audio.Tracks.AggregateVolume.Value;
});
AddStep("duck one", () =>
{
duckOp1 = Game.MusicController.Duck(new DuckParameters
{
DuckVolumeTo = 0.5,
});
});
AddUntilStep("wait for duck to complete", () => Game.Audio.Tracks.AggregateVolume.Value, () => Is.EqualTo(normalVolume * 0.5f).Within(0.01));
AddStep("duck two", () =>
{
duckOp2 = Game.MusicController.Duck(new DuckParameters
{
DuckVolumeTo = 0.2,
});
});
AddUntilStep("wait for duck to complete", () => Game.Audio.Tracks.AggregateVolume.Value, () => Is.EqualTo(normalVolume * 0.2f).Within(0.01));
AddStep("restore one", () => duckOp1.Dispose());
// reverse order, less extreme duck removed so won't change
AddUntilStep("wait for restore to complete", () => Game.Audio.Tracks.AggregateVolume.Value, () => Is.EqualTo(normalVolume * 0.2f).Within(0.01));
AddStep("restore two", () => duckOp2.Dispose());
AddUntilStep("wait for restore to complete", () => Game.Audio.Tracks.AggregateVolume.Value, () => Is.EqualTo(normalVolume).Within(0.01));
}
[Test]
public void TestMultipleDisposalIsNoop()
{
IDisposable duckOp1 = null!;
AddStep("duck", () => duckOp1 = Game.MusicController.Duck());
AddStep("restore", () => duckOp1.Dispose());
AddStep("restore", () => duckOp1.Dispose());
}
[Test]
public void TestMultipleDucksDifferentPieces()
{
IDisposable duckOp1 = null!;
IDisposable duckOp2 = null!;
AddStep("duck volume", () =>
{
duckOp1 = Game.MusicController.Duck(new DuckParameters
{
DuckVolumeTo = 0.2,
DuckCutoffTo = AudioFilter.MAX_LOWPASS_CUTOFF,
DuckDuration = 500,
});
});
AddStep("duck lowpass", () =>
{
duckOp2 = Game.MusicController.Duck(new DuckParameters
{
DuckVolumeTo = 1,
DuckCutoffTo = 300,
DuckDuration = 500,
});
});
AddStep("restore lowpass", () => duckOp2.Dispose());
AddStep("restore volume", () => duckOp1.Dispose());
}
}
}

View File

@ -58,7 +58,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddStep("select difficulty adjust", () => freeModSelectOverlay.SelectedMods.Value = new[] { new OsuModDifficultyAdjust() });
AddWaitStep("wait some", 3);
AddAssert("customisation area not expanded", () => this.ChildrenOfType<ModSettingsArea>().Single().Height == 0);
AddAssert("customisation area not expanded", () => !this.ChildrenOfType<ModCustomisationPanel>().Single().Expanded.Value);
}
[Test]

View File

@ -47,9 +47,9 @@ namespace osu.Game.Tests.Visual.UserInterface
public void TestArrowDirection()
{
AddStep("Set upwards", () => button.SetIconDirection(true));
AddAssert("Icon facing upwards", () => button.Icon.Scale.Y == -1);
AddUntilStep("Icon facing upwards", () => button.Icon.Scale.Y == -1);
AddStep("Set downwards", () => button.SetIconDirection(false));
AddAssert("Icon facing downwards", () => button.Icon.Scale.Y == 1);
AddUntilStep("Icon facing downwards", () => button.Icon.Scale.Y == 1);
}
private partial class TestButton : CommentRepliesButton

View File

@ -0,0 +1,66 @@
// 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 NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Overlays;
using osu.Game.Overlays.Mods;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu.Mods;
namespace osu.Game.Tests.Visual.UserInterface
{
public partial class TestSceneModCustomisationPanel : OsuManualInputManagerTestScene
{
[Cached]
private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Aquamarine);
private ModCustomisationPanel panel = null!;
[SetUp]
public void SetUp() => Schedule(() =>
{
Child = new Container
{
RelativeSizeAxes = Axes.Both,
Padding = new MarginPadding(20f),
Child = panel = new ModCustomisationPanel
{
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
Width = 400f,
State = { Value = Visibility.Visible },
SelectedMods = { BindTarget = SelectedMods },
}
};
});
[Test]
public void TestDisplay()
{
AddStep("set DT", () =>
{
SelectedMods.Value = new[] { new OsuModDoubleTime() };
panel.Enabled.Value = panel.Expanded.Value = true;
});
AddStep("set DA", () =>
{
SelectedMods.Value = new Mod[] { new OsuModDifficultyAdjust() };
panel.Enabled.Value = panel.Expanded.Value = true;
});
AddStep("set FL+WU+DA+AD", () =>
{
SelectedMods.Value = new Mod[] { new OsuModFlashlight(), new ModWindUp(), new OsuModDifficultyAdjust(), new OsuModApproachDifferent() };
panel.Enabled.Value = panel.Expanded.Value = true;
});
AddStep("set empty", () =>
{
SelectedMods.Value = Array.Empty<Mod>();
panel.Enabled.Value = panel.Expanded.Value = false;
});
}
}
}

View File

@ -7,7 +7,6 @@ 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.Input;
@ -56,6 +55,7 @@ namespace osu.Game.Tests.Visual.UserInterface
AddStep("clear contents", Clear);
AddStep("reset ruleset", () => Ruleset.Value = rulesetStore.GetRuleset(0));
AddStep("reset mods", () => SelectedMods.SetDefault());
AddStep("reset config", () => configManager.SetValue(OsuSetting.ModSelectTextSearchStartsActive, true));
AddStep("set beatmap", () => Beatmap.Value = CreateWorkingBeatmap(new OsuRuleset().RulesetInfo));
AddStep("set up presets", () =>
{
@ -225,7 +225,7 @@ namespace osu.Game.Tests.Visual.UserInterface
AddStep("dismiss mod customisation via toggle", () =>
{
InputManager.MoveMouseTo(modSelectOverlay.CustomisationButton.AsNonNull());
InputManager.MoveMouseTo(modSelectOverlay.ChildrenOfType<ModCustomisationHeader>().Single());
InputManager.Click(MouseButton.Left);
});
assertCustomisationToggleState(disabled: false, active: false);
@ -258,7 +258,7 @@ namespace osu.Game.Tests.Visual.UserInterface
}
[Test]
public void TestDismissCustomisationViaDimmedArea()
public void TestDismissCustomisationViaClickingAway()
{
createScreen();
assertCustomisationToggleState(disabled: true, active: false);
@ -266,18 +266,23 @@ namespace osu.Game.Tests.Visual.UserInterface
AddStep("select difficulty adjust via panel", () => getPanelForMod(typeof(OsuModDifficultyAdjust)).TriggerClick());
assertCustomisationToggleState(disabled: false, active: true);
AddStep("move mouse to settings area", () => InputManager.MoveMouseTo(this.ChildrenOfType<ModSettingsArea>().Single()));
AddStep("move mouse to dimmed area", () =>
{
InputManager.MoveMouseTo(new Vector2(
modSelectOverlay.ScreenSpaceDrawQuad.TopLeft.X,
(modSelectOverlay.ScreenSpaceDrawQuad.TopLeft.Y + modSelectOverlay.ScreenSpaceDrawQuad.BottomLeft.Y) / 2));
});
AddStep("move mouse to search bar", () => InputManager.MoveMouseTo(modSelectOverlay.ChildrenOfType<ShearedSearchTextBox>().Single()));
AddStep("click", () => InputManager.Click(MouseButton.Left));
assertCustomisationToggleState(disabled: false, active: false);
}
AddStep("move mouse to first mod panel", () => InputManager.MoveMouseTo(modSelectOverlay.ChildrenOfType<ModPanel>().First()));
AddAssert("first mod panel is hovered", () => modSelectOverlay.ChildrenOfType<ModPanel>().First().IsHovered);
[Test]
public void TestDismissCustomisationWhenHidingOverlay()
{
createScreen();
assertCustomisationToggleState(disabled: true, active: false);
AddStep("select difficulty adjust via panel", () => getPanelForMod(typeof(OsuModDifficultyAdjust)).TriggerClick());
assertCustomisationToggleState(disabled: false, active: true);
AddStep("hide overlay", () => modSelectOverlay.Hide());
AddStep("show overlay again", () => modSelectOverlay.Show());
assertCustomisationToggleState(disabled: false, active: false);
}
/// <summary>
@ -339,7 +344,7 @@ namespace osu.Game.Tests.Visual.UserInterface
createScreen();
changeRuleset(0);
AddStep("Select all fun mods", () =>
AddStep("Select all difficulty-increase mods", () =>
{
modSelectOverlay.ChildrenOfType<ModColumn>()
.Single(c => c.ModType == ModType.DifficultyIncrease)
@ -641,13 +646,15 @@ namespace osu.Game.Tests.Visual.UserInterface
AddStep("select DT", () => SelectedMods.Value = new Mod[] { new OsuModDoubleTime() });
AddAssert("DT selected", () => modSelectOverlay.ChildrenOfType<ModPanel>().Count(panel => panel.Active.Value), () => Is.EqualTo(1));
AddStep("open customisation area", () => modSelectOverlay.CustomisationButton!.TriggerClick());
assertCustomisationToggleState(false, true);
AddStep("open customisation area", () => modSelectOverlay.ChildrenOfType<ModCustomisationHeader>().Single().TriggerClick());
assertCustomisationToggleState(disabled: false, active: true);
AddStep("hover over mod settings slider", () =>
{
var slider = modSelectOverlay.ChildrenOfType<ModSettingsArea>().Single().ChildrenOfType<OsuSliderBar<double>>().First();
var slider = modSelectOverlay.ChildrenOfType<ModCustomisationPanel>().Single().ChildrenOfType<OsuSliderBar<double>>().First();
InputManager.MoveMouseTo(slider);
});
AddStep("press right arrow", () => InputManager.PressKey(Key.Right));
AddAssert("DT speed changed", () => !SelectedMods.Value.OfType<OsuModDoubleTime>().Single().SpeedChange.IsDefault);
@ -744,9 +751,10 @@ namespace osu.Game.Tests.Visual.UserInterface
AddStep("select difficulty adjust via panel", () => getPanelForMod(typeof(OsuModDifficultyAdjust)).TriggerClick());
assertCustomisationToggleState(disabled: false, active: true);
AddAssert("back button disabled", () => !modSelectOverlay.BackButton.Enabled.Value);
AddStep("dismiss customisation area", () => InputManager.Key(Key.Escape));
AddAssert("mod select still visible", () => modSelectOverlay.State.Value == Visibility.Visible);
AddStep("click back button", () =>
{
InputManager.MoveMouseTo(modSelectOverlay.BackButton);
@ -755,6 +763,19 @@ namespace osu.Game.Tests.Visual.UserInterface
AddAssert("mod select hidden", () => modSelectOverlay.State.Value == Visibility.Hidden);
}
[Test]
public void TestCloseViaToggleModSelectionBinding()
{
createScreen();
changeRuleset(0);
AddStep("select difficulty adjust via panel", () => getPanelForMod(typeof(OsuModDifficultyAdjust)).TriggerClick());
assertCustomisationToggleState(disabled: false, active: true);
AddStep("press F1", () => InputManager.Key(Key.F1));
AddAssert("mod select hidden", () => modSelectOverlay.State.Value == Visibility.Hidden);
}
/// <summary>
/// Covers columns hiding/unhiding on changes of <see cref="ModSelectOverlay.IsValidMod"/>.
/// </summary>
@ -870,8 +891,8 @@ namespace osu.Game.Tests.Visual.UserInterface
// it is instrumental in the reproduction of the failure scenario that this test is supposed to cover.
AddStep("force collection", GC.Collect);
AddStep("open customisation area", () => modSelectOverlay.CustomisationButton!.TriggerClick());
AddStep("reset half time speed to default", () => modSelectOverlay.ChildrenOfType<ModSettingsArea>().Single()
AddStep("open customisation area", () => modSelectOverlay.ChildrenOfType<ModCustomisationHeader>().Single().TriggerClick());
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));
@ -883,24 +904,91 @@ namespace osu.Game.Tests.Visual.UserInterface
createScreen();
AddStep("select DT + HD + DF", () => SelectedMods.Value = new Mod[] { new OsuModDoubleTime(), new OsuModHidden(), new OsuModDeflate() });
AddStep("open customisation panel", () => this.ChildrenOfType<ModCustomisationHeader>().Single().TriggerClick());
AddAssert("mod settings order: DT, HD, DF", () =>
{
var columns = this.ChildrenOfType<ModSettingsArea>().Single().ChildrenOfType<ModSettingsArea.ModSettingsColumn>();
var columns = this.ChildrenOfType<ModCustomisationSection>();
return columns.ElementAt(0).Mod is OsuModDoubleTime &&
columns.ElementAt(1).Mod is OsuModHidden &&
columns.ElementAt(2).Mod is OsuModDeflate;
});
AddStep("replace DT with NC", () => SelectedMods.Value = SelectedMods.Value.Where(m => m is not ModDoubleTime).Append(new OsuModNightcore()).ToList());
AddStep("replace DT with NC", () =>
{
SelectedMods.Value = SelectedMods.Value.Where(m => m is not ModDoubleTime).Append(new OsuModNightcore()).ToList();
this.ChildrenOfType<ModCustomisationHeader>().Single().TriggerClick();
});
AddAssert("mod settings order: NC, HD, DF", () =>
{
var columns = this.ChildrenOfType<ModSettingsArea>().Single().ChildrenOfType<ModSettingsArea.ModSettingsColumn>();
var columns = this.ChildrenOfType<ModCustomisationSection>();
return columns.ElementAt(0).Mod is OsuModNightcore &&
columns.ElementAt(1).Mod is OsuModHidden &&
columns.ElementAt(2).Mod is OsuModDeflate;
});
}
[Test]
public void TestOpeningCustomisationHidesPresetPopover()
{
createScreen();
AddStep("select DT", () => SelectedMods.Value = new Mod[] { new OsuModDoubleTime() });
AddStep("click new preset", () =>
{
InputManager.MoveMouseTo(this.ChildrenOfType<AddPresetButton>().Single());
InputManager.Click(MouseButton.Left);
});
AddAssert("preset popover shown", () => this.ChildrenOfType<AddPresetPopover>().SingleOrDefault()?.IsPresent, () => Is.True);
AddStep("click customisation header", () =>
{
InputManager.MoveMouseTo(this.ChildrenOfType<ModCustomisationHeader>().Single());
InputManager.Click(MouseButton.Left);
});
AddUntilStep("preset popover hidden", () => this.ChildrenOfType<AddPresetPopover>().SingleOrDefault()?.IsPresent, () => Is.Not.True);
AddAssert("customisation panel shown", () => this.ChildrenOfType<ModCustomisationPanel>().Single().State.Value, () => Is.EqualTo(Visibility.Visible));
}
[Test]
public void TestCustomisationPanelAbsorbsInput([Values] bool textSearchStartsActive)
{
AddStep($"text search starts active = {textSearchStartsActive}", () => configManager.SetValue(OsuSetting.ModSelectTextSearchStartsActive, textSearchStartsActive));
createScreen();
AddStep("select DT", () => SelectedMods.Value = new Mod[] { new OsuModDoubleTime() });
AddStep("open customisation panel", () => this.ChildrenOfType<ModCustomisationHeader>().Single().TriggerClick());
AddAssert("search lost focus", () => !this.ChildrenOfType<ShearedSearchTextBox>().Single().HasFocus);
AddStep("press tab", () => InputManager.Key(Key.Tab));
AddAssert("search still not focused", () => !this.ChildrenOfType<ShearedSearchTextBox>().Single().HasFocus);
AddStep("press q", () => InputManager.Key(Key.Q));
AddAssert("easy not selected", () => SelectedMods.Value.Single() is OsuModDoubleTime);
// the "deselect all mods" action is intentionally disabled when customisation panel is open to not conflict with pressing backspace to delete characters in a textbox.
// this is supposed to be handled by the textbox itself especially since it's focused and thus prioritised in input queue,
// but it's not for some reason, and figuring out why is probably not going to be a pleasant experience (read TextBox.OnKeyDown for a head start).
AddStep("press backspace", () => InputManager.Key(Key.BackSpace));
AddAssert("mods not deselected", () => SelectedMods.Value.Single() is OsuModDoubleTime);
AddStep("move mouse to scroll bar", () => InputManager.MoveMouseTo(modSelectOverlay.ChildrenOfType<ModSelectOverlay.ColumnScrollContainer>().Single().ScreenSpaceDrawQuad.BottomLeft + new Vector2(10f, -5f)));
AddStep("scroll down", () => InputManager.ScrollVerticalBy(-10f));
AddAssert("column not scrolled", () => modSelectOverlay.ChildrenOfType<ModSelectOverlay.ColumnScrollContainer>().Single().IsScrolledToStart());
AddStep("press mouse", () => InputManager.PressButton(MouseButton.Left));
AddAssert("search still not focused", () => !this.ChildrenOfType<ShearedSearchTextBox>().Single().HasFocus);
AddStep("release mouse", () => InputManager.ReleaseButton(MouseButton.Left));
AddAssert("customisation panel closed by click", () => !this.ChildrenOfType<ModCustomisationPanel>().Single().Expanded.Value);
if (textSearchStartsActive)
AddAssert("search focused", () => this.ChildrenOfType<ShearedSearchTextBox>().Single().HasFocus);
else
AddAssert("search still not focused", () => !this.ChildrenOfType<ShearedSearchTextBox>().Single().HasFocus);
}
private void waitForColumnLoad() => AddUntilStep("all column content loaded", () =>
modSelectOverlay.ChildrenOfType<ModColumn>().Any()
&& modSelectOverlay.ChildrenOfType<ModColumn>().All(column => column.IsLoaded && column.ItemsLoaded)
@ -915,8 +1003,8 @@ namespace osu.Game.Tests.Visual.UserInterface
private void assertCustomisationToggleState(bool disabled, bool active)
{
AddAssert($"customisation toggle is {(disabled ? "" : "not ")}disabled", () => modSelectOverlay.CustomisationButton.AsNonNull().Active.Disabled == disabled);
AddAssert($"customisation toggle is {(active ? "" : "not ")}active", () => modSelectOverlay.CustomisationButton.AsNonNull().Active.Value == active);
AddUntilStep($"customisation panel is {(disabled ? "" : "not ")}disabled", () => modSelectOverlay.ChildrenOfType<ModCustomisationPanel>().Single().Enabled.Value == !disabled);
AddAssert($"customisation panel is {(active ? "" : "not ")}active", () => modSelectOverlay.ChildrenOfType<ModCustomisationPanel>().Single().Expanded.Value == active);
}
private T getSelectedMod<T>() where T : Mod => SelectedMods.Value.OfType<T>().Single();
@ -929,7 +1017,6 @@ namespace osu.Game.Tests.Visual.UserInterface
protected override bool ShowPresets => true;
public new ShearedButton BackButton => base.BackButton;
public new ShearedToggleButton? CustomisationButton => base.CustomisationButton;
}
private class TestUnimplementedMod : Mod

View File

@ -1,42 +0,0 @@
// 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.
#nullable disable
using System;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Overlays;
using osu.Game.Overlays.Mods;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu.Mods;
namespace osu.Game.Tests.Visual.UserInterface
{
[TestFixture]
public partial class TestSceneModSettingsArea : OsuTestScene
{
[Cached]
private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Green);
[Test]
public void TestModToggleArea()
{
ModSettingsArea modSettingsArea = null;
AddStep("create content", () => Child = new Container
{
RelativeSizeAxes = Axes.Both,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Child = modSettingsArea = new ModSettingsArea()
});
AddStep("set DT", () => modSettingsArea.SelectedMods.Value = new[] { new OsuModDoubleTime() });
AddStep("set DA", () => modSettingsArea.SelectedMods.Value = new Mod[] { new OsuModDifficultyAdjust() });
AddStep("set FL+WU+DA+AD", () => modSettingsArea.SelectedMods.Value = new Mod[] { new OsuModFlashlight(), new ModWindUp(), new OsuModDifficultyAdjust(), new OsuModApproachDifferent() });
AddStep("set empty", () => modSettingsArea.SelectedMods.Value = Array.Empty<Mod>());
}
}
}

View File

@ -163,8 +163,8 @@ namespace osu.Game.Collections
public CollectionDropdownHeader()
{
Height = 25;
Icon.Size = new Vector2(16);
Foreground.Padding = new MarginPadding { Top = 4, Bottom = 4, Left = 8, Right = 4 };
Chevron.Size = new Vector2(12);
Foreground.Padding = new MarginPadding { Top = 4, Bottom = 4, Left = 8, Right = 8 };
}
}

View File

@ -1,17 +1,17 @@
// 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 osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites;
using osu.Game.Audio.Effects;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
using osu.Game.Overlays;
using osuTK;
namespace osu.Game.Collections
@ -21,11 +21,14 @@ namespace osu.Game.Collections
private const double enter_duration = 500;
private const double exit_duration = 200;
private AudioFilter lowPassFilter = null!;
protected override string PopInSampleName => @"UI/overlay-big-pop-in";
protected override string PopOutSampleName => @"UI/overlay-big-pop-out";
private IDisposable? duckOperation;
[Resolved]
private MusicController? musicController { get; set; }
public ManageCollectionsDialog()
{
Anchor = Anchor.Centre;
@ -39,7 +42,7 @@ namespace osu.Game.Collections
}
[BackgroundDependencyLoader]
private void load(OsuColour colours, AudioManager audio)
private void load(OsuColour colours)
{
Children = new Drawable[]
{
@ -110,19 +113,25 @@ namespace osu.Game.Collections
},
}
}
},
lowPassFilter = new AudioFilter(audio.TrackMixer)
}
};
}
public override bool IsPresent => base.IsPresent
// Safety for low pass filter potentially getting stuck in applied state due to
// transforms on `this` causing children to no longer be updated.
|| lowPassFilter.IsAttached;
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
duckOperation?.Dispose();
}
protected override void PopIn()
{
lowPassFilter.CutoffTo(300, 100, Easing.OutCubic);
duckOperation = musicController?.Duck(new DuckParameters
{
DuckVolumeTo = 1,
DuckDuration = 100,
RestoreDuration = 100,
});
this.FadeIn(enter_duration, Easing.OutQuint);
this.ScaleTo(0.9f).Then().ScaleTo(1f, enter_duration, Easing.OutQuint);
}
@ -131,7 +140,7 @@ namespace osu.Game.Collections
{
base.PopOut();
lowPassFilter.CutoffTo(AudioFilter.MAX_LOWPASS_CUTOFF, 100, Easing.InCubic);
duckOperation?.Dispose();
this.FadeOut(exit_duration, Easing.OutQuint);
this.ScaleTo(0.9f, exit_duration);

View File

@ -1,6 +1,7 @@
// 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.Diagnostics;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Audio;
@ -30,6 +31,12 @@ namespace osu.Game.Graphics.UserInterface
protected override DropdownMenu CreateMenu() => new OsuDropdownMenu();
public OsuDropdown()
{
if (Header is OsuDropdownHeader osuHeader)
osuHeader.Dropdown = this;
}
public bool OnPressed(KeyBindingPressEvent<GlobalAction> e)
{
if (e.Repeat) return false;
@ -307,7 +314,9 @@ namespace osu.Game.Graphics.UserInterface
set => Text.Text = value;
}
protected readonly SpriteIcon Icon;
protected readonly SpriteIcon Chevron;
public OsuDropdown<T>? Dropdown { get; set; }
public OsuDropdownHeader()
{
@ -341,7 +350,7 @@ namespace osu.Game.Graphics.UserInterface
Origin = Anchor.CentreLeft,
RelativeSizeAxes = Axes.X,
},
Icon = new SpriteIcon
Chevron = new SpriteIcon
{
Icon = FontAwesome.Solid.ChevronDown,
Anchor = Anchor.CentreRight,
@ -365,6 +374,9 @@ namespace osu.Game.Graphics.UserInterface
{
base.LoadComplete();
if (Dropdown != null)
Dropdown.Menu.StateChanged += _ => updateChevron();
SearchBar.State.ValueChanged += _ => updateColour();
Enabled.BindValueChanged(_ => updateColour());
updateColour();
@ -392,16 +404,23 @@ namespace osu.Game.Graphics.UserInterface
if (SearchBar.State.Value == Visibility.Visible)
{
Icon.Colour = hovered ? hoveredColour.Lighten(0.5f) : Colour4.White;
Chevron.Colour = hovered ? hoveredColour.Lighten(0.5f) : Colour4.White;
Background.Colour = unhoveredColour;
}
else
{
Icon.Colour = Color4.White;
Chevron.Colour = Color4.White;
Background.Colour = hovered ? hoveredColour : unhoveredColour;
}
}
private void updateChevron()
{
Debug.Assert(Dropdown != null);
bool open = Dropdown.Menu.State == MenuState.Open;
Chevron.ScaleTo(open ? new Vector2(1f, -1f) : Vector2.One, 300, Easing.OutQuint);
}
protected override DropdownSearchBar CreateSearchBar() => new OsuDropdownSearchBar
{
Padding = new MarginPadding { Right = 26 },

View File

@ -75,6 +75,16 @@ namespace osu.Game.Localisation
/// </summary>
public static LocalisableString UnrankedExplanation => new TranslatableString(getKey(@"unranked_explanation"), @"Performance points will not be granted due to active mods.");
/// <summary>
/// "Customise"
/// </summary>
public static LocalisableString CustomisationPanelHeader => new TranslatableString(getKey(@"customisation_panel_header"), @"Customise");
/// <summary>
/// "No mod selected which can be customised."
/// </summary>
public static LocalisableString CustomisationPanelDisabledReason => new TranslatableString(getKey(@"customisation_panel_disabled_reason"), @"No mod selected which can be customised.");
private static string getKey(string key) => $@"{prefix}:{key}";
}
}

View File

@ -24,6 +24,7 @@ namespace osu.Game.Overlays.Comments.Buttons
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Size = new Vector2(12),
Icon = FontAwesome.Solid.ChevronDown
};
}
@ -38,11 +39,12 @@ namespace osu.Game.Overlays.Comments.Buttons
base.LoadComplete();
Action = Expanded.Toggle;
Expanded.BindValueChanged(onExpandedChanged, true);
FinishTransforms(true);
}
private void onExpandedChanged(ValueChangedEvent<bool> expanded)
{
icon.Icon = expanded.NewValue ? FontAwesome.Solid.ChevronUp : FontAwesome.Solid.ChevronDown;
icon.ScaleTo(expanded.NewValue ? new Vector2(1f, -1f) : Vector2.One, 300, Easing.OutQuint);
}
}
}

View File

@ -89,7 +89,7 @@ namespace osu.Game.Overlays.Comments.Buttons
background.Colour = colourProvider.Background2;
}
protected void SetIconDirection(bool upwards) => icon.ScaleTo(new Vector2(1, upwards ? -1 : 1));
protected void SetIconDirection(bool upwards) => icon.ScaleTo(upwards ? new Vector2(1f, -1f) : Vector2.One, 300, Easing.OutQuint);
public void ToggleTextVisibility(bool visible) => text.FadeTo(visible ? 1 : 0, 200, Easing.OutQuint);

View File

@ -3,7 +3,6 @@
#nullable disable
using System;
using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Audio.Sample;
@ -11,7 +10,6 @@ using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Input.Events;
using osu.Game.Audio.Effects;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
@ -57,7 +55,6 @@ namespace osu.Game.Overlays.Dialog
private Sample tickSample;
private Sample confirmSample;
private double lastTickPlaybackTime;
private AudioFilter lowPassFilter = null!;
private bool mouseDown;
[BackgroundDependencyLoader]
@ -65,8 +62,6 @@ namespace osu.Game.Overlays.Dialog
{
tickSample = audio.Samples.Get(@"UI/dialog-dangerous-tick");
confirmSample = audio.Samples.Get(@"UI/dialog-dangerous-select");
AddInternal(lowPassFilter = new AudioFilter(audio.SampleMixer));
}
protected override void LoadComplete()
@ -75,15 +70,8 @@ namespace osu.Game.Overlays.Dialog
Progress.BindValueChanged(progressChanged, true);
}
protected override void AbortConfirm()
{
lowPassFilter.CutoffTo(AudioFilter.MAX_LOWPASS_CUTOFF);
base.AbortConfirm();
}
protected override void Confirm()
{
lowPassFilter.CutoffTo(AudioFilter.MAX_LOWPASS_CUTOFF);
confirmSample?.Play();
base.Confirm();
}
@ -123,8 +111,6 @@ namespace osu.Game.Overlays.Dialog
private void progressChanged(ValueChangedEvent<double> progress)
{
lowPassFilter.Cutoff = Math.Max(1, (int)(progress.NewValue * AudioFilter.MAX_LOWPASS_CUTOFF * 0.5));
if (progress.NewValue < progress.OldValue)
return;

View File

@ -3,16 +3,16 @@
#nullable disable
using System;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Overlays.Dialog;
using osu.Game.Graphics.Containers;
using osu.Game.Input.Bindings;
using System.Linq;
using JetBrains.Annotations;
using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Input.Events;
using osu.Game.Audio.Effects;
namespace osu.Game.Overlays
{
@ -23,15 +23,16 @@ namespace osu.Game.Overlays
protected override string PopInSampleName => "UI/dialog-pop-in";
protected override string PopOutSampleName => "UI/dialog-pop-out";
private AudioFilter lowPassFilter;
[Resolved]
private MusicController musicController { get; set; }
public PopupDialog CurrentDialog { get; private set; }
public override bool IsPresent => Scheduler.HasPendingTasks
|| dialogContainer.Children.Count > 0
// Safety for low pass filter potentially getting stuck in applied state due to
// transforms on `this` causing children to no longer be updated.
|| lowPassFilter.IsAttached;
|| dialogContainer.Children.Count > 0;
[CanBeNull]
private IDisposable duckOperation;
public DialogOverlay()
{
@ -49,10 +50,10 @@ namespace osu.Game.Overlays
Origin = Anchor.Centre;
}
[BackgroundDependencyLoader]
private void load(AudioManager audio)
protected override void Dispose(bool isDisposing)
{
AddInternal(lowPassFilter = new AudioFilter(audio.TrackMixer));
base.Dispose(isDisposing);
duckOperation?.Dispose();
}
public void Push(PopupDialog dialog)
@ -105,13 +106,18 @@ namespace osu.Game.Overlays
protected override void PopIn()
{
lowPassFilter.CutoffTo(300, 100, Easing.OutCubic);
duckOperation = musicController?.Duck(new DuckParameters
{
DuckVolumeTo = 1,
DuckDuration = 100,
RestoreDuration = 100,
});
}
protected override void PopOut()
{
base.PopOut();
lowPassFilter.CutoffTo(AudioFilter.MAX_LOWPASS_CUTOFF, 100, Easing.InCubic);
duckOperation?.Dispose();
// PopOut gets called initially, but we only want to hide dialog when we have been loaded and are present.
if (IsLoaded && CurrentDialog?.State.Value == Visibility.Visible)

View File

@ -0,0 +1,95 @@
// 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 osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites;
using osuTK;
using osu.Game.Localisation;
namespace osu.Game.Overlays.Mods
{
public partial class ModCustomisationHeader : OsuHoverContainer
{
private Box background = null!;
private SpriteIcon icon = null!;
[Resolved]
private OverlayColourProvider colourProvider { get; set; } = null!;
protected override IEnumerable<Drawable> EffectTargets => new[] { background };
public readonly BindableBool Expanded = new BindableBool();
public ModCustomisationHeader()
{
Action = Expanded.Toggle;
Enabled.Value = false;
}
[BackgroundDependencyLoader]
private void load()
{
CornerRadius = 10f;
Masking = true;
Children = new Drawable[]
{
background = new Box
{
RelativeSizeAxes = Axes.Both,
},
new OsuSpriteText
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Text = ModSelectOverlayStrings.CustomisationPanelHeader,
UseFullGlyphHeight = false,
Font = OsuFont.Torus.With(size: 20f, weight: FontWeight.SemiBold),
Margin = new MarginPadding { Left = 20f },
},
new Container
{
Anchor = Anchor.CentreRight,
Origin = Anchor.CentreRight,
Size = new Vector2(16f),
Margin = new MarginPadding { Right = 20f },
Child = icon = new SpriteIcon
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Icon = FontAwesome.Solid.ChevronDown,
RelativeSizeAxes = Axes.Both,
}
}
};
IdleColour = colourProvider.Dark3;
HoverColour = colourProvider.Light4;
}
protected override void LoadComplete()
{
base.LoadComplete();
Enabled.BindValueChanged(e =>
{
TooltipText = e.NewValue
? string.Empty
: ModSelectOverlayStrings.CustomisationPanelDisabledReason;
}, true);
Expanded.BindValueChanged(v =>
{
icon.ScaleTo(v.NewValue ? new Vector2(1, -1) : Vector2.One, 300, Easing.OutQuint);
}, true);
}
}
}

View File

@ -0,0 +1,211 @@
// 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 osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Effects;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Input.Bindings;
using osu.Framework.Input.Events;
using osu.Game.Configuration;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Input.Bindings;
using osu.Game.Rulesets.Mods;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Overlays.Mods
{
public partial class ModCustomisationPanel : OverlayContainer, IKeyBindingHandler<GlobalAction>
{
private const float header_height = 42f;
private const float content_vertical_padding = 20f;
private const float content_border_thickness = 2f;
private Container content = null!;
private OsuScrollContainer scrollContainer = null!;
private FillFlowContainer sectionsFlow = null!;
[Resolved]
private OverlayColourProvider colourProvider { get; set; } = null!;
public readonly BindableBool Enabled = new BindableBool();
public readonly BindableBool Expanded = new BindableBool();
public Bindable<IReadOnlyList<Mod>> SelectedMods { get; } = new Bindable<IReadOnlyList<Mod>>(Array.Empty<Mod>());
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => true;
// Handle{Non}PositionalInput controls whether the panel should act as a blocking layer on the screen. only block when the panel is expanded.
// These properties are used because they correctly handle blocking/unblocking hover when mouse is pointing at a drawable outside
// (returning Expanded.Value to OnHover or overriding Block{Non}PositionalInput doesn't work).
public override bool HandlePositionalInput => Expanded.Value;
public override bool HandleNonPositionalInput => Expanded.Value;
[BackgroundDependencyLoader]
private void load()
{
RelativeSizeAxes = Axes.Y;
InternalChildren = new Drawable[]
{
new ModCustomisationHeader
{
Depth = float.MinValue,
RelativeSizeAxes = Axes.X,
Height = header_height,
Enabled = { BindTarget = Enabled },
Expanded = { BindTarget = Expanded },
},
content = new FocusGrabbingContainer
{
RelativeSizeAxes = Axes.X,
BorderColour = colourProvider.Dark3,
BorderThickness = content_border_thickness,
CornerRadius = 10f,
Masking = true,
EdgeEffect = new EdgeEffectParameters
{
Type = EdgeEffectType.Shadow,
Offset = new Vector2(0f, 5f),
Radius = 20f,
Roundness = 5f,
Colour = Color4.Black.Opacity(0.25f),
},
Expanded = { BindTarget = Expanded },
Children = new Drawable[]
{
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = colourProvider.Dark4,
},
scrollContainer = new OsuScrollContainer(Direction.Vertical)
{
RelativeSizeAxes = Axes.X,
Margin = new MarginPadding
{
Top = header_height + content_border_thickness,
Bottom = content_border_thickness
},
Child = sectionsFlow = new FillFlowContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Spacing = new Vector2(0f, 40f),
Margin = new MarginPadding
{
Top = content_vertical_padding,
Bottom = 5f + content_vertical_padding
},
}
}
}
}
};
}
protected override void LoadComplete()
{
base.LoadComplete();
Enabled.BindValueChanged(e =>
{
this.FadeColour(OsuColour.Gray(e.NewValue ? 1f : 0.6f), 300, Easing.OutQuint);
}, true);
Expanded.BindValueChanged(_ => updateDisplay(), true);
SelectedMods.BindValueChanged(_ => updateMods(), true);
FinishTransforms(true);
}
protected override void PopIn() => this.FadeIn(300, Easing.OutQuint);
protected override void PopOut() => this.FadeOut(300, Easing.OutQuint);
protected override bool OnClick(ClickEvent e)
{
Expanded.Value = false;
return base.OnClick(e);
}
protected override bool OnKeyDown(KeyDownEvent e) => true;
protected override bool OnScroll(ScrollEvent e) => true;
public bool OnPressed(KeyBindingPressEvent<GlobalAction> e)
{
switch (e.Action)
{
case GlobalAction.Back:
Expanded.Value = false;
return true;
}
return false;
}
public void OnReleased(KeyBindingReleaseEvent<GlobalAction> e)
{
}
private void updateDisplay()
{
content.ClearTransforms();
if (Expanded.Value)
{
content.AutoSizeDuration = 400;
content.AutoSizeEasing = Easing.OutQuint;
content.AutoSizeAxes = Axes.Y;
content.FadeIn(120, Easing.OutQuint);
}
else
{
content.AutoSizeAxes = Axes.None;
content.ResizeHeightTo(header_height, 400, Easing.OutQuint);
content.FadeOut(400, Easing.OutSine);
}
}
private void updateMods()
{
Expanded.Value = false;
sectionsFlow.Clear();
// Importantly, the selected mods bindable is already ordered by the mod select overlay (following the order of mod columns and panels).
// Using AsOrdered produces a slightly different order (e.g. DT and NC no longer becoming adjacent),
// which breaks user expectations when interacting with the overlay.
foreach (var mod in SelectedMods.Value)
{
var settings = mod.CreateSettingsControls().ToList();
if (settings.Count > 0)
sectionsFlow.Add(new ModCustomisationSection(mod, settings));
}
}
protected override void Update()
{
base.Update();
scrollContainer.Height = Math.Min(scrollContainer.AvailableContent, DrawHeight - header_height);
}
private partial class FocusGrabbingContainer : InputBlockingContainer
{
public IBindable<bool> Expanded { get; } = new BindableBool();
public override bool RequestsFocus => Expanded.Value;
public override bool AcceptsFocus => Expanded.Value;
}
}
}

View File

@ -0,0 +1,82 @@
// 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 osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.UI;
using osuTK;
namespace osu.Game.Overlays.Mods
{
public partial class ModCustomisationSection : CompositeDrawable
{
public readonly Mod Mod;
private readonly IReadOnlyList<Drawable> settings;
public ModCustomisationSection(Mod mod, IReadOnlyList<Drawable> settings)
{
Mod = mod;
this.settings = settings;
}
[BackgroundDependencyLoader]
private void load()
{
RelativeSizeAxes = Axes.X;
AutoSizeAxes = Axes.Y;
FillFlowContainer flow;
InternalChild = flow = new FillFlowContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Direction = FillDirection.Vertical,
Spacing = new Vector2(0f, 8f),
Padding = new MarginPadding { Left = 7f },
Children = new Drawable[]
{
new Container
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Padding = new MarginPadding { Left = 20f, Right = 27f },
Margin = new MarginPadding { Bottom = 4f },
Children = new Drawable[]
{
new OsuSpriteText
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Text = Mod.Name,
Font = OsuFont.TorusAlternate.With(size: 20, weight: FontWeight.SemiBold),
},
new ModSwitchTiny(Mod)
{
Anchor = Anchor.CentreRight,
Origin = Anchor.CentreRight,
Active = { Value = true },
Scale = new Vector2(0.5f),
}
}
},
}
};
flow.AddRange(settings);
}
protected override void LoadComplete()
{
base.LoadComplete();
FinishTransforms(true);
}
}
}

View File

@ -29,6 +29,7 @@ using osu.Game.Localisation;
using osu.Game.Rulesets.Mods;
using osu.Game.Utils;
using osuTK;
using osuTK.Graphics;
using osuTK.Input;
namespace osu.Game.Overlays.Mods
@ -109,15 +110,6 @@ namespace osu.Game.Overlays.Mods
protected virtual IEnumerable<ShearedButton> CreateFooterButtons()
{
if (AllowCustomisation)
{
yield return CustomisationButton = new ShearedToggleButton(BUTTON_WIDTH)
{
Text = ModSelectOverlayStrings.ModCustomisation,
Active = { BindTarget = customisationVisible }
};
}
yield return deselectAllModsButton = new DeselectAllModsButton(this);
}
@ -125,10 +117,8 @@ namespace osu.Game.Overlays.Mods
public IEnumerable<ModState> AllAvailableMods => AvailableMods.Value.SelectMany(pair => pair.Value);
private readonly BindableBool customisationVisible = new BindableBool();
private Bindable<bool> textSearchStartsActive = null!;
private ModSettingsArea modSettingsArea = null!;
private ColumnScrollContainer columnScroll = null!;
private ColumnFlowContainer columnFlow = null!;
private FillFlowContainer<ShearedButton> footerButtonFlow = null!;
@ -138,9 +128,9 @@ namespace osu.Game.Overlays.Mods
private Container aboveColumnsContent = null!;
private RankingInformationDisplay? rankingInformationDisplay;
private BeatmapAttributesDisplay? beatmapAttributesDisplay;
private ModCustomisationPanel customisationPanel = null!;
protected ShearedButton BackButton { get; private set; } = null!;
protected ShearedToggleButton? CustomisationButton { get; private set; }
protected SelectAllModsButton? SelectAllModsButton { get; set; }
private Sample? columnAppearSample;
@ -173,70 +163,67 @@ namespace osu.Game.Overlays.Mods
columnAppearSample = audio.Samples.Get(@"SongSelect/mod-column-pop-in");
AddRange(new Drawable[]
MainAreaContent.Add(new OsuContextMenuContainer
{
new ClickToReturnContainer
RelativeSizeAxes = Axes.Both,
Child = new PopoverContainer
{
RelativeSizeAxes = Axes.Both,
HandleMouse = { BindTarget = customisationVisible },
OnClicked = () => customisationVisible.Value = false
},
modSettingsArea = new ModSettingsArea
{
Anchor = Anchor.BottomCentre,
Origin = Anchor.BottomCentre,
Height = 0
},
});
MainAreaContent.AddRange(new Drawable[]
{
aboveColumnsContent = new Container
{
RelativeSizeAxes = Axes.X,
Height = RankingInformationDisplay.HEIGHT,
Padding = new MarginPadding { Horizontal = 100 },
Child = SearchTextBox = new ShearedSearchTextBox
Children = new Drawable[]
{
HoldFocus = false,
Width = 300
}
},
new OsuContextMenuContainer
{
RelativeSizeAxes = Axes.Both,
Child = new PopoverContainer
{
Padding = new MarginPadding
new Container
{
Top = RankingInformationDisplay.HEIGHT + PADDING,
Bottom = PADDING
},
RelativeSizeAxes = Axes.Both,
RelativePositionAxes = Axes.Both,
Children = new Drawable[]
{
columnScroll = new ColumnScrollContainer
Padding = new MarginPadding
{
RelativeSizeAxes = Axes.Both,
Masking = false,
ClampExtension = 100,
ScrollbarOverlapsContent = false,
Child = columnFlow = new ColumnFlowContainer
Top = RankingInformationDisplay.HEIGHT + PADDING,
Bottom = PADDING
},
RelativeSizeAxes = Axes.Both,
RelativePositionAxes = Axes.Both,
Children = new Drawable[]
{
columnScroll = new ColumnScrollContainer
{
Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomLeft,
Direction = FillDirection.Horizontal,
Shear = new Vector2(OsuGame.SHEAR, 0),
RelativeSizeAxes = Axes.Y,
AutoSizeAxes = Axes.X,
Margin = new MarginPadding { Horizontal = 70 },
Padding = new MarginPadding { Bottom = 10 },
ChildrenEnumerable = createColumns()
RelativeSizeAxes = Axes.Both,
Masking = false,
ClampExtension = 100,
ScrollbarOverlapsContent = false,
Child = columnFlow = new ColumnFlowContainer
{
Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomLeft,
Direction = FillDirection.Horizontal,
Shear = new Vector2(OsuGame.SHEAR, 0),
RelativeSizeAxes = Axes.Y,
AutoSizeAxes = Axes.X,
Margin = new MarginPadding { Horizontal = 70 },
Padding = new MarginPadding { Bottom = 10 },
ChildrenEnumerable = createColumns()
}
}
}
},
aboveColumnsContent = new Container
{
RelativeSizeAxes = Axes.Both,
Padding = new MarginPadding { Horizontal = 100, Bottom = 15f },
Children = new Drawable[]
{
SearchTextBox = new ShearedSearchTextBox
{
HoldFocus = false,
Width = 300,
},
customisationPanel = new ModCustomisationPanel
{
Anchor = Anchor.TopRight,
Origin = Anchor.TopRight,
Width = 400,
State = { Value = Visibility.Visible },
}
}
}
}
},
}
});
@ -320,7 +307,7 @@ namespace osu.Game.Overlays.Mods
// This is an optimisation to prevent refreshing the available settings controls when it can be
// reasonably assumed that the settings panel is never to be displayed (e.g. FreeModSelectOverlay).
if (AllowCustomisation)
((IBindable<IReadOnlyList<Mod>>)modSettingsArea.SelectedMods).BindTo(SelectedMods);
((IBindable<IReadOnlyList<Mod>>)customisationPanel.SelectedMods).BindTo(SelectedMods);
SelectedMods.BindValueChanged(_ =>
{
@ -347,7 +334,7 @@ namespace osu.Game.Overlays.Mods
}
}, true);
customisationVisible.BindValueChanged(_ => updateCustomisationVisualState(), true);
customisationPanel.Expanded.BindValueChanged(_ => updateCustomisationVisualState(), true);
SearchTextBox.Current.BindValueChanged(query =>
{
@ -390,6 +377,7 @@ namespace osu.Game.Overlays.Mods
footerContentFlow.LayoutDuration = 200;
footerContentFlow.LayoutEasing = Easing.OutQuint;
footerContentFlow.Direction = screenIsntWideEnough ? FillDirection.Vertical : FillDirection.Horizontal;
aboveColumnsContent.Padding = aboveColumnsContent.Padding with { Bottom = screenIsntWideEnough ? 70f : 15f };
}
}
@ -491,7 +479,7 @@ namespace osu.Game.Overlays.Mods
private void updateCustomisation()
{
if (CustomisationButton == null)
if (!AllowCustomisation)
return;
bool anyCustomisableModActive = false;
@ -506,41 +494,32 @@ namespace osu.Game.Overlays.Mods
if (anyCustomisableModActive)
{
customisationVisible.Disabled = false;
customisationPanel.Enabled.Value = true;
if (anyModPendingConfiguration && !customisationVisible.Value)
customisationVisible.Value = true;
if (anyModPendingConfiguration)
customisationPanel.Expanded.Value = true;
}
else
{
if (customisationVisible.Value)
customisationVisible.Value = false;
customisationVisible.Disabled = true;
customisationPanel.Expanded.Value = false;
customisationPanel.Enabled.Value = false;
}
}
private void updateCustomisationVisualState()
{
const double transition_duration = 300;
MainAreaContent.FadeColour(customisationVisible.Value ? Colour4.Gray : Colour4.White, transition_duration, Easing.InOutCubic);
foreach (var button in footerButtonFlow)
if (customisationPanel.Expanded.Value)
{
if (button != CustomisationButton)
button.Enabled.Value = !customisationVisible.Value;
}
float modAreaHeight = customisationVisible.Value ? ModSettingsArea.HEIGHT : 0;
modSettingsArea.ResizeHeightTo(modAreaHeight, transition_duration, Easing.InOutCubic);
TopLevelContent.MoveToY(-modAreaHeight, transition_duration, Easing.InOutCubic);
if (customisationVisible.Value)
columnScroll.FadeColour(OsuColour.Gray(0.5f), 400, Easing.OutQuint);
SearchTextBox.FadeColour(OsuColour.Gray(0.5f), 400, Easing.OutQuint);
SearchTextBox.KillFocus();
}
else
{
columnScroll.FadeColour(Color4.White, 400, Easing.OutQuint);
SearchTextBox.FadeColour(Color4.White, 400, Easing.OutQuint);
setTextBoxFocus(textSearchStartsActive.Value);
}
}
/// <summary>
@ -693,6 +672,8 @@ namespace osu.Game.Overlays.Mods
if (!allFiltered)
nonFilteredColumnCount += 1;
}
customisationPanel.Expanded.Value = false;
}
#endregion
@ -706,16 +687,12 @@ namespace osu.Game.Overlays.Mods
switch (e.Action)
{
// If the customisation panel is expanded, the back action will be handled by it first.
case GlobalAction.Back:
// Pressing the back binding should only go back one step at a time.
hideOverlay(false);
return true;
// This is handled locally here because this overlay is being registered at the game level
// and therefore takes away keyboard focus from the screen stack.
case GlobalAction.ToggleModSelection:
// Pressing toggle should completely hide the overlay in one shot.
hideOverlay(true);
hideOverlay();
return true;
// This is handled locally here due to conflicts in input handling between the search text box and the deselect all mods button.
@ -723,7 +700,7 @@ namespace osu.Game.Overlays.Mods
// wherein activating the binding will both change the contents of the search text box and deselect all mods.
case GlobalAction.DeselectAllMods:
{
if (!SearchTextBox.HasFocus)
if (!SearchTextBox.HasFocus && !customisationPanel.Expanded.Value)
{
deselectAllModsButton.TriggerClick();
return true;
@ -738,7 +715,7 @@ namespace osu.Game.Overlays.Mods
// If there is no search in progress, it should exit the dialog (a bit weird, but this is the expectation from stable).
if (string.IsNullOrEmpty(SearchTerm))
{
hideOverlay(true);
hideOverlay();
return true;
}
@ -756,19 +733,7 @@ namespace osu.Game.Overlays.Mods
return base.OnPressed(e);
void hideOverlay(bool immediate)
{
if (customisationVisible.Value)
{
Debug.Assert(CustomisationButton != null);
CustomisationButton.TriggerClick();
if (!immediate)
return;
}
BackButton.TriggerClick();
}
void hideOverlay() => BackButton.TriggerClick();
}
/// <inheritdoc cref="IKeyBindingHandler{PlatformAction}"/>
@ -795,6 +760,9 @@ namespace osu.Game.Overlays.Mods
if (e.Repeat || e.Key != Key.Tab)
return false;
if (customisationPanel.Expanded.Value)
return true;
// TODO: should probably eventually support typical platform search shortcuts (`Ctrl-F`, `/`)
setTextBoxFocus(!SearchTextBox.HasFocus);
return true;
@ -967,38 +935,5 @@ namespace osu.Game.Overlays.Mods
updateState();
}
}
/// <summary>
/// A container which blocks and handles input, managing the "return from customisation" state change.
/// </summary>
private partial class ClickToReturnContainer : Container
{
public BindableBool HandleMouse { get; } = new BindableBool();
public Action? OnClicked { get; set; }
public override bool HandlePositionalInput => base.HandlePositionalInput && HandleMouse.Value;
protected override bool Handle(UIEvent e)
{
if (!HandleMouse.Value)
return base.Handle(e);
switch (e)
{
case ClickEvent:
OnClicked?.Invoke();
return true;
case HoverEvent:
return false;
case MouseEvent:
return true;
}
return base.Handle(e);
}
}
}
}

View File

@ -1,189 +0,0 @@
// 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 osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Input.Events;
using osu.Game.Configuration;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.UI;
using osuTK;
namespace osu.Game.Overlays.Mods
{
public partial class ModSettingsArea : CompositeDrawable
{
public Bindable<IReadOnlyList<Mod>> SelectedMods { get; } = new Bindable<IReadOnlyList<Mod>>(Array.Empty<Mod>());
public const float HEIGHT = 250;
private readonly Box background;
private readonly FillFlowContainer modSettingsFlow;
[Resolved]
private OverlayColourProvider colourProvider { get; set; } = null!;
public override bool AcceptsFocus => true;
public ModSettingsArea()
{
RelativeSizeAxes = Axes.X;
Height = HEIGHT;
Anchor = Anchor.BottomRight;
Origin = Anchor.BottomRight;
InternalChild = new Container
{
RelativeSizeAxes = Axes.Both,
Masking = true,
Children = new Drawable[]
{
background = new Box
{
RelativeSizeAxes = Axes.Both
},
new OsuScrollContainer(Direction.Horizontal)
{
RelativeSizeAxes = Axes.Both,
ScrollbarOverlapsContent = false,
ClampExtension = 100,
Child = modSettingsFlow = new FillFlowContainer
{
AutoSizeAxes = Axes.X,
RelativeSizeAxes = Axes.Y,
Padding = new MarginPadding { Vertical = 7, Horizontal = 70 },
Spacing = new Vector2(7),
Direction = FillDirection.Horizontal
}
}
}
};
}
[BackgroundDependencyLoader]
private void load()
{
background.Colour = colourProvider.Dark3;
}
protected override void LoadComplete()
{
base.LoadComplete();
SelectedMods.BindValueChanged(_ => updateMods(), true);
}
private void updateMods()
{
modSettingsFlow.Clear();
// Importantly, the selected mods bindable is already ordered by the mod select overlay (following the order of mod columns and panels).
// Using AsOrdered produces a slightly different order (e.g. DT and NC no longer becoming adjacent),
// which breaks user expectations when interacting with the overlay.
foreach (var mod in SelectedMods.Value)
{
var settings = mod.CreateSettingsControls().ToList();
if (settings.Count > 0)
{
if (modSettingsFlow.Any())
{
modSettingsFlow.Add(new Box
{
RelativeSizeAxes = Axes.Y,
Width = 2,
Colour = colourProvider.Dark4,
});
}
modSettingsFlow.Add(new ModSettingsColumn(mod, settings));
}
}
}
protected override bool OnMouseDown(MouseDownEvent e) => true;
protected override bool OnHover(HoverEvent e) => true;
public partial class ModSettingsColumn : CompositeDrawable
{
public readonly Mod Mod;
public ModSettingsColumn(Mod mod, IEnumerable<Drawable> settingsControls)
{
Mod = mod;
Width = 250;
RelativeSizeAxes = Axes.Y;
Padding = new MarginPadding { Bottom = 7 };
InternalChild = new GridContainer
{
RelativeSizeAxes = Axes.Both,
RowDimensions = new[]
{
new Dimension(GridSizeMode.AutoSize),
new Dimension(GridSizeMode.Absolute, 10),
new Dimension()
},
Content = new[]
{
new Drawable[]
{
new FillFlowContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Direction = FillDirection.Horizontal,
Spacing = new Vector2(7),
Children = new Drawable[]
{
new ModSwitchTiny(mod)
{
Active = { Value = true },
Scale = new Vector2(0.6f),
Origin = Anchor.CentreLeft,
Anchor = Anchor.CentreLeft
},
new OsuSpriteText
{
Text = mod.Name,
Font = OsuFont.Default.With(size: 16, weight: FontWeight.SemiBold),
Origin = Anchor.CentreLeft,
Anchor = Anchor.CentreLeft,
Margin = new MarginPadding { Bottom = 2 }
}
}
}
},
new[] { Empty() },
new Drawable[]
{
new OsuScrollContainer(Direction.Vertical)
{
RelativeSizeAxes = Axes.Both,
ClampExtension = 100,
Child = new FillFlowContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Padding = new MarginPadding { Right = 7 },
ChildrenEnumerable = settingsControls,
Spacing = new Vector2(0, 7)
}
}
}
}
};
}
}
}
}

View File

@ -53,8 +53,8 @@ namespace osu.Game.Overlays.Music
{
CornerRadius = 5;
Height = 30;
Icon.Size = new Vector2(14);
Icon.Margin = new MarginPadding(0);
Chevron.Size = new Vector2(14);
Chevron.Margin = new MarginPadding(0);
Foreground.Padding = new MarginPadding { Top = 4, Bottom = 4, Left = 10, Right = 10 };
EdgeEffect = new EdgeEffectParameters
{

View File

@ -13,6 +13,7 @@ using osu.Framework.Graphics.Audio;
using osu.Framework.Graphics.Containers;
using osu.Framework.Logging;
using osu.Framework.Threading;
using osu.Game.Audio.Effects;
using osu.Game.Beatmaps;
using osu.Game.Database;
using osu.Game.Rulesets.Mods;
@ -59,6 +60,17 @@ namespace osu.Game.Overlays
[Resolved]
private RealmAccess realm { get; set; } = null!;
private readonly BindableDouble audioDuckVolume = new BindableDouble(1);
private AudioFilter audioDuckFilter = null!;
[BackgroundDependencyLoader]
private void load(AudioManager audio)
{
AddInternal(audioDuckFilter = new AudioFilter(audio.TrackMixer));
audio.Tracks.AddAdjustment(AdjustableProperty.Volume, audioDuckVolume);
}
protected override void LoadComplete()
{
base.LoadComplete();
@ -246,6 +258,54 @@ namespace osu.Game.Overlays
onSuccess?.Invoke();
});
private readonly List<DuckParameters> duckOperations = new List<DuckParameters>();
/// <summary>
/// Applies ducking, attenuating the volume and/or low-pass cutoff of the currently playing track to make headroom for effects (or just to apply an effect).
/// </summary>
/// <returns>A <see cref="IDisposable"/> which will restore the duck operation when disposed.</returns>
public IDisposable Duck(DuckParameters? parameters = null)
{
parameters ??= new DuckParameters();
duckOperations.Add(parameters);
DuckParameters volumeOperation = duckOperations.MinBy(p => p.DuckVolumeTo)!;
DuckParameters lowPassOperation = duckOperations.MinBy(p => p.DuckCutoffTo)!;
audioDuckFilter.CutoffTo(lowPassOperation.DuckCutoffTo, lowPassOperation.DuckDuration, lowPassOperation.DuckEasing);
this.TransformBindableTo(audioDuckVolume, volumeOperation.DuckVolumeTo, volumeOperation.DuckDuration, volumeOperation.DuckEasing);
return new InvokeOnDisposal(restoreDucking);
void restoreDucking() => Schedule(() =>
{
if (!duckOperations.Remove(parameters))
return;
DuckParameters? restoreVolumeOperation = duckOperations.MinBy(p => p.DuckVolumeTo);
DuckParameters? restoreLowPassOperation = duckOperations.MinBy(p => p.DuckCutoffTo);
// If another duck operation is in the list, restore ducking to its level, else reset back to defaults.
audioDuckFilter.CutoffTo(restoreLowPassOperation?.DuckCutoffTo ?? AudioFilter.MAX_LOWPASS_CUTOFF, parameters.RestoreDuration, parameters.RestoreEasing);
this.TransformBindableTo(audioDuckVolume, restoreVolumeOperation?.DuckVolumeTo ?? 1, parameters.RestoreDuration, parameters.RestoreEasing);
});
}
/// <summary>
/// A convenience method that ducks the currently playing track, then after a delay, restores automatically.
/// </summary>
/// <param name="delayUntilRestore">A delay in milliseconds which defines how long to delay restoration after ducking completes.</param>
/// <param name="parameters">Parameters defining the ducking operation.</param>
public void DuckMomentarily(double delayUntilRestore, DuckParameters? parameters = null)
{
parameters ??= new DuckParameters();
IDisposable duckOperation = Duck(parameters);
Scheduler.AddDelayed(() => duckOperation.Dispose(), delayUntilRestore);
}
private bool next()
{
if (beatmap.Disabled || !AllowTrackControl.Value)
@ -419,6 +479,45 @@ namespace osu.Game.Overlays
}
}
public class DuckParameters
{
/// <summary>
/// The duration of the ducking transition in milliseconds.
/// Defaults to 100 ms.
/// </summary>
public double DuckDuration = 100;
/// <summary>
/// The final volume which should be reached during ducking, when 0 is silent and 1 is original volume.
/// Defaults to 25%.
/// </summary>
public double DuckVolumeTo = 0.25;
/// <summary>
/// The low-pass cutoff frequency which should be reached during ducking. If not required, set to <see cref="AudioFilter.MAX_LOWPASS_CUTOFF"/>.
/// Defaults to 300 Hz.
/// </summary>
public int DuckCutoffTo = 300;
/// <summary>
/// The easing curve to be applied during ducking.
/// Defaults to <see cref="Easing.Out"/>.
/// </summary>
public Easing DuckEasing = Easing.Out;
/// <summary>
/// The duration of the restoration transition in milliseconds.
/// Defaults to 500 ms.
/// </summary>
public double RestoreDuration = 500;
/// <summary>
/// The easing curve to be applied during restoration.
/// Defaults to <see cref="Easing.In"/>.
/// </summary>
public Easing RestoreEasing = Easing.In;
}
public enum TrackChangeDirection
{
None,

View File

@ -118,7 +118,7 @@ namespace osu.Game.Overlays.News.Sidebar
Expanded.BindValueChanged(open =>
{
icon.Scale = new Vector2(1, open.NewValue ? -1 : 1);
icon.ScaleTo(open.NewValue ? new Vector2(1f, -1f) : Vector2.One, 300, Easing.OutQuint);
}, true);
}
}

View File

@ -163,7 +163,7 @@ namespace osu.Game.Overlays
LastScrollTarget.BindValueChanged(target =>
{
spriteIcon.RotateTo(target.NewValue != null ? 180 : 0, fade_duration, Easing.OutQuint);
spriteIcon.ScaleTo(target.NewValue != null ? new Vector2(1f, -1f) : Vector2.One, fade_duration, Easing.OutQuint);
TooltipText = target.NewValue != null ? CommonStrings.ButtonsBackToPrevious : CommonStrings.ButtonsBackToTop;
}, true);
}

View File

@ -50,12 +50,13 @@ namespace osu.Game.Overlays.Profile.Header.Components
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Size = new Vector2(10.5f, 12)
Size = new Vector2(10.5f, 12),
Icon = FontAwesome.Solid.ChevronDown,
};
CoverExpanded.BindValueChanged(visible => updateState(visible.NewValue), true);
}
private void updateState(bool detailsVisible) => icon.Icon = detailsVisible ? FontAwesome.Solid.ChevronUp : FontAwesome.Solid.ChevronDown;
private void updateState(bool detailsVisible) => icon.ScaleTo(detailsVisible ? new Vector2(1f, -1f) : Vector2.One, 300, Easing.OutQuint);
}
}

View File

@ -200,7 +200,7 @@ namespace osu.Game.Overlays.Rankings
Text.Font = OsuFont.GetFont(size: 15);
Text.Padding = new MarginPadding { Vertical = 1.5f }; // osu-web line-height difference compensation
Foreground.Padding = new MarginPadding { Horizontal = 10, Vertical = 15 };
Margin = Icon.Margin = new MarginPadding(0);
Margin = Chevron.Margin = new MarginPadding(0);
}
[BackgroundDependencyLoader]

View File

@ -3,8 +3,12 @@
#nullable disable
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Audio.Sample;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
@ -21,6 +25,13 @@ namespace osu.Game.Overlays.Toolbar
{
protected Drawable ModeButtonLine { get; private set; }
[Resolved]
private MusicController musicController { get; set; }
private readonly Dictionary<RulesetInfo, Sample> rulesetSelectionSample = new Dictionary<RulesetInfo, Sample>();
private readonly Dictionary<RulesetInfo, SampleChannel> rulesetSelectionChannel = new Dictionary<RulesetInfo, SampleChannel>();
private Sample defaultSelectSample;
public ToolbarRulesetSelector()
{
RelativeSizeAxes = Axes.Y;
@ -28,7 +39,7 @@ namespace osu.Game.Overlays.Toolbar
}
[BackgroundDependencyLoader]
private void load()
private void load(AudioManager audio)
{
AddRangeInternal(new[]
{
@ -54,6 +65,13 @@ namespace osu.Game.Overlays.Toolbar
}
},
});
foreach (var r in Rulesets.AvailableRulesets)
rulesetSelectionSample[r] = audio.Samples.Get($@"UI/ruleset-select-{r.ShortName}");
defaultSelectSample = audio.Samples.Get(@"UI/default-select");
Current.ValueChanged += playRulesetSelectionSample;
}
protected override void LoadComplete()
@ -84,6 +102,29 @@ namespace osu.Game.Overlays.Toolbar
}
}
private void playRulesetSelectionSample(ValueChangedEvent<RulesetInfo> r)
{
// Don't play sample on first setting of value
if (r.OldValue == null)
return;
var channel = rulesetSelectionSample[r.NewValue]?.GetChannel();
// Skip sample choking and ducking for the default/fallback sample
if (channel == null)
{
defaultSelectSample.Play();
return;
}
foreach (var pair in rulesetSelectionChannel)
pair.Value?.Stop();
rulesetSelectionChannel[r.NewValue] = channel;
channel.Play();
musicController?.DuckMomentarily(500, new DuckParameters { DuckDuration = 0 });
}
public override bool HandleNonPositionalInput => !Current.Disabled && base.HandleNonPositionalInput;
public override bool HandlePositionalInput => !Current.Disabled && base.HandlePositionalInput;

View File

@ -2,8 +2,6 @@
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Audio.Sample;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.UserInterface;
@ -19,8 +17,6 @@ namespace osu.Game.Overlays.Toolbar
{
private readonly RulesetButton ruleset;
private Sample? selectSample;
public ToolbarRulesetTabButton(RulesetInfo value)
: base(value)
{
@ -38,18 +34,10 @@ namespace osu.Game.Overlays.Toolbar
ruleset.SetIcon(rInstance.CreateIcon());
}
[BackgroundDependencyLoader]
private void load(AudioManager audio)
{
selectSample = audio.Samples.Get($@"UI/ruleset-select-{Value.ShortName}");
}
protected override void OnActivated() => ruleset.Active = true;
protected override void OnDeactivated() => ruleset.Active = false;
protected override void OnActivatedByUser() => selectSample?.Play();
private partial class RulesetButton : ToolbarButton
{
protected override HoverSounds CreateHoverSounds(HoverSampleSet sampleSet) => new HoverSounds();

View File

@ -71,7 +71,10 @@ namespace osu.Game.Screens.Edit.Components.Menus
});
}
protected override Framework.Graphics.UserInterface.Menu CreateSubMenu() => new SubMenu();
protected override Framework.Graphics.UserInterface.Menu CreateSubMenu() => new SubMenu
{
MaxHeight = MaxHeight,
};
protected override DrawableMenuItem CreateDrawableMenuItem(MenuItem item) => new DrawableEditorBarMenuItem(item);
@ -143,7 +146,10 @@ namespace osu.Game.Screens.Edit.Components.Menus
BackgroundColour = colourProvider.Background2;
}
protected override Framework.Graphics.UserInterface.Menu CreateSubMenu() => new SubMenu();
protected override Framework.Graphics.UserInterface.Menu CreateSubMenu() => new SubMenu
{
MaxHeight = MaxHeight,
};
protected override DrawableMenuItem CreateDrawableMenuItem(MenuItem item)
{

View File

@ -105,7 +105,7 @@ namespace osu.Game.Screens.Edit.Components
protected override void Dispose(bool isDisposing)
{
Track.Value?.RemoveAdjustment(AdjustableProperty.Frequency, tempoAdjustment);
Track.Value?.RemoveAdjustment(AdjustableProperty.Tempo, tempoAdjustment);
base.Dispose(isDisposing);
}

View File

@ -343,6 +343,7 @@ namespace osu.Game.Screens.Edit
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
RelativeSizeAxes = Axes.Both,
MaxHeight = 600,
Items = new[]
{
new MenuItem(CommonStrings.MenuBarFile)

View File

@ -154,7 +154,7 @@ namespace osu.Game.Screens.Edit
/// The current time of this clock, include any active transform seeks performed via <see cref="SeekSmoothlyTo"/>.
/// </summary>
public double CurrentTimeAccurate =>
Transforms.OfType<TransformSeek>().FirstOrDefault()?.EndValue ?? CurrentTime;
Transforms.OfType<TransformSeek>().LastOrDefault()?.EndValue ?? CurrentTime;
public double CurrentTime => underlyingClock.CurrentTime;

View File

@ -72,7 +72,11 @@ namespace osu.Game.Screens.Edit.GameplayTest
foreach (var hitObject in enumerateHitObjects(DrawableRuleset.Objects, editorState.Time))
{
var judgement = hitObject.Judgement;
var result = new JudgementResult(hitObject, judgement) { Type = judgement.MaxResult };
var result = new JudgementResult(hitObject, judgement)
{
Type = judgement.MaxResult,
GameplayRate = GameplayClockContainer.GetTrueGameplayRate(),
};
HealthProcessor.ApplyResult(result);
ScoreProcessor.ApplyResult(result);

View File

@ -36,7 +36,7 @@
</PackageReference>
<PackageReference Include="Realm" Version="11.5.0" />
<PackageReference Include="ppy.osu.Framework" Version="2024.702.0" />
<PackageReference Include="ppy.osu.Game.Resources" Version="2024.622.0" />
<PackageReference Include="ppy.osu.Game.Resources" Version="2024.705.0" />
<PackageReference Include="Sentry" Version="4.3.0" />
<!-- Held back due to 0.34.0 failing AOT compilation on ZstdSharp.dll dependency. -->
<PackageReference Include="SharpCompress" Version="0.36.0" />