diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModPanel.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModPanel.cs new file mode 100644 index 0000000000..bdf3f98863 --- /dev/null +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModPanel.cs @@ -0,0 +1,42 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +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.Osu.Mods; +using osuTK; + +namespace osu.Game.Tests.Visual.UserInterface +{ + [TestFixture] + public class TestSceneModPanel : OsuManualInputManagerTestScene + { + [Cached] + private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Green); + + [Test] + public void TestVariousPanels() + { + AddStep("create content", () => Child = new FillFlowContainer + { + Width = 300, + AutoSizeAxes = Axes.Y, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Spacing = new Vector2(0, 5), + Children = new[] + { + new ModPanel(new OsuModHalfTime()), + new ModPanel(new OsuModFlashlight()), + new ModPanel(new OsuModAutoplay()), + new ModPanel(new OsuModAlternate()), + new ModPanel(new OsuModApproachDifferent()) + } + }); + } + } +} diff --git a/osu.Game/Overlays/Mods/ModPanel.cs b/osu.Game/Overlays/Mods/ModPanel.cs new file mode 100644 index 0000000000..4d08b22017 --- /dev/null +++ b/osu.Game/Overlays/Mods/ModPanel.cs @@ -0,0 +1,272 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using osu.Framework.Allocation; +using osu.Framework.Audio; +using osu.Framework.Audio.Sample; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Colour; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Input.Events; +using osu.Framework.Utils; +using osu.Game.Graphics; +using osu.Game.Graphics.Containers; +using osu.Game.Graphics.Sprites; +using osu.Game.Graphics.UserInterface; +using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.UI; +using osuTK; +using osuTK.Input; + +#nullable enable + +namespace osu.Game.Overlays.Mods +{ + public class ModPanel : OsuClickableContainer + { + public Mod Mod { get; } + public BindableBool Active { get; } = new BindableBool(); + + protected readonly Box Background; + protected readonly Container SwitchContainer; + protected readonly Container MainContentContainer; + protected readonly Box TextBackground; + protected readonly FillFlowContainer TextFlow; + + [Resolved] + protected OverlayColourProvider ColourProvider { get; private set; } = null!; + + protected const double TRANSITION_DURATION = 150; + protected const float SHEAR_X = 0.2f; + + protected const float HEIGHT = 42; + protected const float CORNER_RADIUS = 7; + protected const float IDLE_SWITCH_WIDTH = 54; + protected const float EXPANDED_SWITCH_WIDTH = 70; + + private Colour4 activeColour; + private Colour4 activeHoverColour; + + private Sample? sampleOff; + private Sample? sampleOn; + + public ModPanel(Mod mod) + { + Mod = mod; + + RelativeSizeAxes = Axes.X; + Height = 42; + + // all below properties are applied to `Content` rather than the `ModPanel` in its entirety + // to allow external components to set these properties on the panel without affecting + // its "internal" appearance. + Content.Masking = true; + Content.CornerRadius = CORNER_RADIUS; + Content.BorderThickness = 2; + Content.Shear = new Vector2(SHEAR_X, 0); + + Children = new Drawable[] + { + Background = new Box + { + RelativeSizeAxes = Axes.Both + }, + SwitchContainer = new Container + { + RelativeSizeAxes = Axes.Y, + Child = new ModSwitchSmall(mod) + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Active = { BindTarget = Active }, + Shear = new Vector2(-SHEAR_X, 0), + Scale = new Vector2(HEIGHT / ModSwitchSmall.DEFAULT_SIZE) + } + }, + MainContentContainer = new Container + { + RelativeSizeAxes = Axes.Both, + Child = new Container + { + RelativeSizeAxes = Axes.Both, + Masking = true, + CornerRadius = CORNER_RADIUS, + Children = new Drawable[] + { + TextBackground = new Box + { + RelativeSizeAxes = Axes.Both + }, + TextFlow = new FillFlowContainer + { + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding + { + Horizontal = 17.5f, + Vertical = 4 + }, + Direction = FillDirection.Vertical, + Children = new[] + { + new OsuSpriteText + { + Text = mod.Name, + Font = OsuFont.TorusAlternate.With(size: 18, weight: FontWeight.SemiBold), + Shear = new Vector2(-SHEAR_X, 0), + Margin = new MarginPadding + { + Left = -18 * SHEAR_X + } + }, + new OsuSpriteText + { + Text = mod.Description, + Font = OsuFont.Default.With(size: 12), + RelativeSizeAxes = Axes.X, + Truncate = true, + Shear = new Vector2(-SHEAR_X, 0) + } + } + } + } + } + } + }; + + Action = Active.Toggle; + } + + [BackgroundDependencyLoader] + private void load(AudioManager audio, OsuColour colours) + { + sampleOn = audio.Samples.Get(@"UI/check-on"); + sampleOff = audio.Samples.Get(@"UI/check-off"); + + activeColour = colours.ForModType(Mod.Type); + activeHoverColour = activeColour.Lighten(0.3f); + } + + protected override HoverSounds CreateHoverSounds(HoverSampleSet sampleSet) => new HoverSounds(sampleSet); + + protected override void LoadComplete() + { + base.LoadComplete(); + Active.BindValueChanged(_ => + { + playStateChangeSamples(); + UpdateState(); + }); + + UpdateState(); + FinishTransforms(true); + } + + private void playStateChangeSamples() + { + if (Active.Value) + sampleOn?.Play(); + else + sampleOff?.Play(); + } + + protected override bool OnHover(HoverEvent e) + { + UpdateState(); + return base.OnHover(e); + } + + protected override void OnHoverLost(HoverLostEvent e) + { + UpdateState(); + base.OnHoverLost(e); + } + + private double? mouseDownTime; + + protected override bool OnMouseDown(MouseDownEvent e) + { + if (e.Button == MouseButton.Left) + mouseDownTime = Time.Current; + return true; + } + + protected override void OnMouseUp(MouseUpEvent e) + { + mouseDownTime = null; + base.OnMouseUp(e); + } + + protected override bool OnDragStart(DragStartEvent e) + { + mouseDownTime = null; + return true; + } + + protected override void Update() + { + base.Update(); + + if (mouseDownTime != null) + { + double startTime = mouseDownTime.Value; + double endTime = startTime + 600; + + float startValue = Active.Value ? EXPANDED_SWITCH_WIDTH : IDLE_SWITCH_WIDTH; + float endValue = IDLE_SWITCH_WIDTH + (EXPANDED_SWITCH_WIDTH - IDLE_SWITCH_WIDTH) * (Active.Value ? 0.2f : 0.8f); + + float targetWidth = Interpolation.ValueAt(Math.Clamp(Time.Current, startTime, endTime), startValue, endValue, startTime, endTime, Easing.OutQuint); + + SwitchContainer.Width = targetWidth; + MainContentContainer.Padding = new MarginPadding + { + Left = targetWidth, + Right = CORNER_RADIUS + }; + } + } + + protected virtual void UpdateState() + { + if (Active.Value) + { + Colour4 backgroundTextColour = IsHovered ? activeHoverColour : activeColour; + Colour4 backgroundColour = Interpolation.ValueAt(0.7f, Colour4.Black, backgroundTextColour, 0, 1); + + Content.TransformTo(nameof(BorderColour), (ColourInfo)backgroundColour, TRANSITION_DURATION, Easing.OutQuint); + Background.FadeColour(backgroundColour, TRANSITION_DURATION, Easing.OutQuint); + SwitchContainer.ResizeWidthTo(EXPANDED_SWITCH_WIDTH, TRANSITION_DURATION, Easing.OutQuint); + MainContentContainer.TransformTo(nameof(Padding), new MarginPadding + { + Left = EXPANDED_SWITCH_WIDTH, + Right = CORNER_RADIUS + }, TRANSITION_DURATION, Easing.OutQuint); + TextBackground.FadeColour(backgroundTextColour, TRANSITION_DURATION, Easing.OutQuint); + TextFlow.FadeColour(ColourProvider.Background6, TRANSITION_DURATION, Easing.OutQuint); + } + else + { + Colour4 backgroundColour = ColourProvider.Background3; + if (IsHovered) + backgroundColour = Interpolation.ValueAt(0.25f, backgroundColour, activeColour, 0, 1); + + Colour4 textBackgroundColour = ColourProvider.Background2; + if (IsHovered) + textBackgroundColour = Interpolation.ValueAt(0.25f, textBackgroundColour, activeColour, 0, 1); + + Content.TransformTo(nameof(BorderColour), ColourInfo.GradientVertical(backgroundColour, textBackgroundColour), TRANSITION_DURATION, Easing.OutQuint); + Background.FadeColour(backgroundColour, TRANSITION_DURATION, Easing.OutQuint); + SwitchContainer.ResizeWidthTo(IDLE_SWITCH_WIDTH, TRANSITION_DURATION, Easing.OutQuint); + MainContentContainer.TransformTo(nameof(Padding), new MarginPadding + { + Left = IDLE_SWITCH_WIDTH, + Right = CORNER_RADIUS + }, TRANSITION_DURATION, Easing.OutQuint); + TextBackground.FadeColour(textBackgroundColour, TRANSITION_DURATION, Easing.OutQuint); + TextFlow.FadeColour(Colour4.White, TRANSITION_DURATION, Easing.OutQuint); + } + } + } +}