diff --git a/osu.Game/GameModes/Menu/Button.cs b/osu.Game/GameModes/Menu/Button.cs
new file mode 100644
index 0000000000..c151a95422
--- /dev/null
+++ b/osu.Game/GameModes/Menu/Button.cs
@@ -0,0 +1,314 @@
+using OpenTK;
+using OpenTK.Graphics;
+using OpenTK.Input;
+using osu.Framework;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Drawables;
+using osu.Framework.Graphics.Primitives;
+using osu.Framework.Graphics.Sprites;
+using osu.Framework.Graphics.Transformations;
+using osu.Framework.Input;
+using osu.Game.Graphics;
+using System;
+
+namespace osu.Game.GameModes.Menu
+{
+ ///
+ /// Button designed specifically for the osu!next main menu.
+ /// In order to correctly flow, we have to use a negative margin on the parent container (due to the parallelogram shape).
+ ///
+ public class Button : AutoSizeContainer, IStateful
+ {
+ private Container iconText;
+ private WedgedBox box;
+ private Color4 colour;
+ private TextAwesome icon;
+ private string internalName;
+ private readonly FontAwesome symbol;
+ private Action clickAction;
+ private readonly float extraWidth;
+ private Key triggerKey;
+ private string text;
+
+ public override Quad ScreenSpaceInputQuad => box.ScreenSpaceInputQuad;
+
+ public Button(string text, string internalName, FontAwesome symbol, Color4 colour, Action clickAction = null, float extraWidth = 0, Key triggerKey = Key.Unknown)
+ {
+ this.internalName = internalName;
+ this.symbol = symbol;
+ this.colour = colour;
+ this.clickAction = clickAction;
+ this.extraWidth = extraWidth;
+ this.triggerKey = triggerKey;
+ this.text = text;
+ }
+
+ public override void Load()
+ {
+ base.Load();
+ Alpha = 0;
+
+ Children = new Drawable[]
+ {
+ box = new WedgedBox(new Vector2(ButtonSystem.button_width + Math.Abs(extraWidth), ButtonSystem.button_area_height), ButtonSystem.wedge_width)
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Colour = colour,
+ Scale = new Vector2(0, 1)
+ },
+ iconText = new AutoSizeContainer
+ {
+ Position = new Vector2(extraWidth / 2, 0),
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Children = new Drawable[]
+ {
+ icon = new TextAwesome
+ {
+ Anchor = Anchor.Centre,
+ TextSize = 30,
+ Position = new Vector2(0, 0),
+ Icon = symbol
+ },
+ new SpriteText
+ {
+ Direction = FlowDirection.HorizontalOnly,
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ TextSize = 16,
+ Position = new Vector2(0, 35),
+ Text = text
+ }
+ }
+ }
+ };
+ }
+
+ protected override bool OnHover(InputState state)
+ {
+ if (State != ButtonState.Expanded) return true;
+
+ //if (OsuGame.Instance.IsActive)
+ // Game.Audio.PlaySamplePositional($@"menu-{internalName}-hover", @"menuclick");
+
+ box.ScaleTo(new Vector2(1.5f, 1), 500, EasingTypes.OutElastic);
+
+ int duration = 0; //(int)(Game.Audio.BeatLength / 2);
+ if (duration == 0) duration = 250;
+
+ icon.ClearTransformations();
+
+ icon.ScaleTo(1, 500, EasingTypes.OutElasticHalf);
+
+ double offset = 0; //(1 - Game.Audio.SyncBeatProgress) * duration;
+ double startTime = Time + offset;
+
+ icon.RotateTo(10, offset, EasingTypes.InOutSine);
+ icon.ScaleTo(new Vector2(1, 0.9f), offset, EasingTypes.Out);
+
+ icon.Transforms.Add(new TransformRotation(Clock)
+ {
+ StartValue = -10,
+ EndValue = 10,
+ StartTime = startTime,
+ EndTime = startTime + duration * 2,
+ Easing = EasingTypes.InOutSine,
+ LoopCount = -1,
+ LoopDelay = duration * 2
+ });
+
+ icon.Transforms.Add(new TransformPosition(Clock)
+ {
+ StartValue = Vector2.Zero,
+ EndValue = new Vector2(0, -10),
+ StartTime = startTime,
+ EndTime = startTime + duration,
+ Easing = EasingTypes.Out,
+ LoopCount = -1,
+ LoopDelay = duration
+ });
+
+ icon.Transforms.Add(new TransformScaleVector(Clock)
+ {
+ StartValue = new Vector2(1, 0.9f),
+ EndValue = Vector2.One,
+ StartTime = startTime,
+ EndTime = startTime + duration,
+ Easing = EasingTypes.Out,
+ LoopCount = -1,
+ LoopDelay = duration
+ });
+
+ icon.Transforms.Add(new TransformPosition(Clock)
+ {
+ StartValue = new Vector2(0, -10),
+ EndValue = Vector2.Zero,
+ StartTime = startTime + duration,
+ EndTime = startTime + duration * 2,
+ Easing = EasingTypes.In,
+ LoopCount = -1,
+ LoopDelay = duration
+ });
+
+ icon.Transforms.Add(new TransformScaleVector(Clock)
+ {
+ StartValue = Vector2.One,
+ EndValue = new Vector2(1, 0.9f),
+ StartTime = startTime + duration,
+ EndTime = startTime + duration * 2,
+ Easing = EasingTypes.In,
+ LoopCount = -1,
+ LoopDelay = duration
+ });
+
+ icon.Transforms.Add(new TransformRotation(Clock)
+ {
+ StartValue = 10,
+ EndValue = -10,
+ StartTime = startTime + duration * 2,
+ EndTime = startTime + duration * 4,
+ Easing = EasingTypes.InOutSine,
+ LoopCount = -1,
+ LoopDelay = duration * 2
+ });
+
+ return true;
+ }
+
+ protected override void OnHoverLost(InputState state)
+ {
+ icon.ClearTransformations();
+ icon.RotateTo(0, 500, EasingTypes.Out);
+ icon.MoveTo(Vector2.Zero, 500, EasingTypes.Out);
+ icon.ScaleTo(0.7f, 500, EasingTypes.OutElasticHalf);
+ icon.ScaleTo(Vector2.One, 200, EasingTypes.Out);
+
+ if (State == ButtonState.Expanded)
+ box.ScaleTo(new Vector2(1, 1), 500, EasingTypes.OutElastic);
+ }
+
+ protected override bool OnMouseDown(InputState state, MouseDownEventArgs args)
+ {
+ trigger();
+ return true;
+ }
+
+ protected override bool OnKeyDown(InputState state, KeyDownEventArgs args)
+ {
+ base.OnKeyDown(state, args);
+
+ if (triggerKey == args.Key && triggerKey != Key.Unknown)
+ {
+ trigger();
+ return true;
+ }
+
+ return false;
+ }
+
+ private void trigger()
+ {
+ //Game.Audio.PlaySamplePositional($@"menu-{internalName}-click", internalName.Contains(@"back") ? @"menuback" : @"menuhit");
+
+ clickAction?.Invoke();
+
+ //box.FlashColour(ColourHelper.Lighten2(colour, 0.7f), 200);
+ }
+
+ public override bool HandleInput => state != ButtonState.Exploded && box.Scale.X >= 0.8f;
+
+ protected override void Update()
+ {
+ iconText.Alpha = MathHelper.Clamp((box.Scale.X - 0.5f) / 0.3f, 0, 1);
+ base.Update();
+ }
+
+ public int ContractStyle;
+
+ ButtonState state;
+ public ButtonState State
+ {
+ get { return state; }
+ set
+ {
+
+ if (state == value)
+ return;
+
+ state = value;
+
+ switch (state)
+ {
+ case ButtonState.Contracted:
+ switch (ContractStyle)
+ {
+ default:
+ box.ScaleTo(new Vector2(0, 1), 500, EasingTypes.OutExpo);
+ FadeOut(500);
+ break;
+ case 1:
+ box.ScaleTo(new Vector2(0, 1), 400, EasingTypes.InSine);
+ FadeOut(800);
+ break;
+ }
+ break;
+ case ButtonState.Expanded:
+ const int expand_duration = 500;
+ box.ScaleTo(new Vector2(1, 1), expand_duration, EasingTypes.OutExpo);
+ FadeIn(expand_duration / 6);
+ break;
+ case ButtonState.Exploded:
+ const int explode_duration = 200;
+ box.ScaleTo(new Vector2(2, 1), explode_duration, EasingTypes.OutExpo);
+ FadeOut(explode_duration / 4 * 3);
+ break;
+ }
+ }
+ }
+
+ ///
+ /// ________
+ /// / /
+ /// / /
+ /// /_______/
+ ///
+ class WedgedBox : Box
+ {
+ float wedgeWidth;
+
+ public WedgedBox(Vector2 boxSize, float wedgeWidth)
+ {
+ Size = boxSize;
+ this.wedgeWidth = wedgeWidth;
+ }
+
+ ///
+ /// Custom DrawQuad used to create the slanted effect.
+ ///
+ protected override Quad DrawQuad
+ {
+ get
+ {
+ Quad q = base.DrawQuad;
+
+ //Will become infinite if we don't limit its maximum size.
+ float wedge = Math.Min(q.Width, wedgeWidth / Scale.X);
+
+ q.TopLeft.X += wedge;
+ q.BottomRight.X -= wedge;
+
+ return q;
+ }
+ }
+ }
+ }
+
+ public enum ButtonState
+ {
+ Contracted,
+ Expanded,
+ Exploded
+ }
+}
diff --git a/osu.Game/GameModes/Menu/ButtonSystem.cs b/osu.Game/GameModes/Menu/ButtonSystem.cs
index 7cc2c85ffb..4624142d6f 100644
--- a/osu.Game/GameModes/Menu/ButtonSystem.cs
+++ b/osu.Game/GameModes/Menu/ButtonSystem.cs
@@ -16,10 +16,11 @@ using osu.Game.Graphics.Containers;
using OpenTK;
using OpenTK.Graphics;
using OpenTK.Input;
+using osu.Framework;
namespace osu.Game.GameModes.Menu
{
- public partial class ButtonSystem : Container
+ public partial class ButtonSystem : Container, IStateful
{
public Action OnEdit;
public Action OnExit;
@@ -32,9 +33,10 @@ namespace osu.Game.GameModes.Menu
private FlowContainerWithOrigin buttonFlow;
- const float button_area_height = 100;
- const float button_width = 140f;
- const float wedge_width = 20;
+ //todo: make these non-internal somehow.
+ internal const float button_area_height = 100;
+ internal const float button_width = 140f;
+ internal const float wedge_width = 20;
public const int EXIT_DELAY = 3000;
@@ -49,15 +51,6 @@ namespace osu.Game.GameModes.Menu
List