mirror of
https://github.com/ppy/osu.git
synced 2026-05-16 02:52:35 +08:00
3931ae3499
This is terrible but I sincerely believe that anything else trying to do this "properly" would be as terrible if not more. - You can try to handle touch events in `MainMenu` but then you'd have to awkwardly still manually hand them off to mouse handlers of the logo / menu button in a weird way for them to do what they're supposed to be doing. So any fix here would likely be smeared across `OsuLogo` and `MainMenuButton` anyway. - The logic in https://github.com/ppy/osu/blob/278a372a907c22f04fe28289c305ef47d5bcef45/osu.Game/Screens/Menu/MainMenu.cs#L517-L520 fundamentally doesn't work with raw touch events because it doesn't check for active touches (easy part) and because drawables do not become "hovered" in the input manager from being touched (hard part) I'm not willing to spend any more time on this.
421 lines
15 KiB
C#
421 lines
15 KiB
C#
// 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.Linq;
|
|
using osu.Framework;
|
|
using osu.Framework.Allocation;
|
|
using osu.Framework.Audio;
|
|
using osu.Framework.Audio.Sample;
|
|
using osu.Framework.Audio.Track;
|
|
using osu.Framework.Graphics;
|
|
using osu.Framework.Graphics.Containers;
|
|
using osu.Framework.Graphics.Shapes;
|
|
using osu.Game.Graphics.Sprites;
|
|
using osuTK;
|
|
using osuTK.Graphics;
|
|
using osuTK.Input;
|
|
using osu.Framework.Extensions.Color4Extensions;
|
|
using osu.Game.Graphics.Containers;
|
|
using osu.Framework.Graphics.Effects;
|
|
using osu.Framework.Graphics.Sprites;
|
|
using osu.Framework.Input.Events;
|
|
using osu.Framework.Input.StateChanges;
|
|
using osu.Framework.Localisation;
|
|
using osu.Game.Beatmaps.ControlPoints;
|
|
|
|
namespace osu.Game.Screens.Menu
|
|
{
|
|
/// <summary>
|
|
/// 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).
|
|
/// </summary>
|
|
public partial class MainMenuButton : BeatSyncedContainer, IStateful<ButtonState>
|
|
{
|
|
public const float BOUNCE_COMPRESSION = 0.9f;
|
|
public const float HOVER_SCALE = 1.2f;
|
|
public const float BOUNCE_ROTATION = 8;
|
|
public event Action<ButtonState>? StateChanged;
|
|
|
|
public readonly Key[] TriggerKeys;
|
|
|
|
protected override Container<Drawable> Content => content;
|
|
private readonly Container content;
|
|
|
|
/// <summary>
|
|
/// The menu state for which we are visible for (assuming only one).
|
|
/// </summary>
|
|
public ButtonSystemState VisibleState
|
|
{
|
|
set
|
|
{
|
|
VisibleStateMin = value;
|
|
VisibleStateMax = value;
|
|
}
|
|
}
|
|
|
|
public ButtonSystemState VisibleStateMin = ButtonSystemState.TopLevel;
|
|
public ButtonSystemState VisibleStateMax = ButtonSystemState.TopLevel;
|
|
|
|
public new MarginPadding Padding
|
|
{
|
|
get => Content.Padding;
|
|
set => Content.Padding = value;
|
|
}
|
|
|
|
protected Vector2 BaseSize { get; init; } = new Vector2(ButtonSystem.BUTTON_WIDTH, ButtonArea.BUTTON_AREA_HEIGHT);
|
|
|
|
private readonly Action<MainMenuButton, UIEvent>? clickAction;
|
|
|
|
private readonly Container background;
|
|
private readonly Drawable backgroundContent;
|
|
private readonly Box boxHoverLayer;
|
|
private readonly SpriteIcon icon;
|
|
|
|
private Vector2 initialSize => BaseSize + Padding.Total;
|
|
|
|
private readonly string sampleName;
|
|
private Sample? sampleClick;
|
|
private Sample? sampleHover;
|
|
private SampleChannel? sampleChannel;
|
|
|
|
public override bool IsPresent => base.IsPresent
|
|
// Allow keyboard interaction based on state rather than waiting for delayed animations.
|
|
|| state == ButtonState.Expanded;
|
|
|
|
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => background.ReceivePositionalInputAt(screenSpacePos);
|
|
|
|
public MainMenuButton(LocalisableString text, string sampleName, IconUsage symbol, Color4 colour, Action<MainMenuButton, UIEvent>? clickAction = null, params Key[] triggerKeys)
|
|
{
|
|
this.sampleName = sampleName;
|
|
this.clickAction = clickAction;
|
|
TriggerKeys = triggerKeys;
|
|
|
|
AutoSizeAxes = Axes.Both;
|
|
Alpha = 0;
|
|
|
|
AddRangeInternal(new Drawable[]
|
|
{
|
|
background = new Container
|
|
{
|
|
// box needs to be always present to ensure the button is always sized correctly for flow
|
|
AlwaysPresent = true,
|
|
Masking = true,
|
|
MaskingSmoothness = 2,
|
|
EdgeEffect = new EdgeEffectParameters
|
|
{
|
|
Type = EdgeEffectType.Shadow,
|
|
Colour = Color4.Black.Opacity(0.2f),
|
|
Roundness = 5,
|
|
Radius = 8,
|
|
},
|
|
Anchor = Anchor.Centre,
|
|
Origin = Anchor.Centre,
|
|
Children = new[]
|
|
{
|
|
backgroundContent = CreateBackground(colour).With(bg =>
|
|
{
|
|
bg.RelativeSizeAxes = Axes.Y;
|
|
bg.Anchor = Anchor.Centre;
|
|
bg.Origin = Anchor.Centre;
|
|
}),
|
|
boxHoverLayer = new Box
|
|
{
|
|
EdgeSmoothness = new Vector2(1.5f, 0),
|
|
RelativeSizeAxes = Axes.Both,
|
|
Blending = BlendingParameters.Additive,
|
|
Colour = Color4.White,
|
|
Depth = float.MinValue,
|
|
Alpha = 0,
|
|
},
|
|
}
|
|
},
|
|
content = new Container
|
|
{
|
|
RelativeSizeAxes = Axes.Both,
|
|
Anchor = Anchor.Centre,
|
|
Origin = Anchor.Centre,
|
|
Children = new Drawable[]
|
|
{
|
|
new OsuSpriteText
|
|
{
|
|
Shadow = true,
|
|
AllowMultiline = false,
|
|
Anchor = Anchor.BottomCentre,
|
|
Origin = Anchor.BottomCentre,
|
|
Margin = new MarginPadding
|
|
{
|
|
Left = -3,
|
|
Bottom = 7,
|
|
},
|
|
Text = text
|
|
},
|
|
icon = new SpriteIcon
|
|
{
|
|
Shadow = true,
|
|
Anchor = Anchor.Centre,
|
|
Origin = Anchor.Centre,
|
|
Size = new Vector2(32),
|
|
Position = new Vector2(0, 0),
|
|
Margin = new MarginPadding { Top = -4 },
|
|
Icon = symbol
|
|
}
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
protected virtual Drawable CreateBackground(Colour4 accentColour) => new Container
|
|
{
|
|
Child = new Box
|
|
{
|
|
EdgeSmoothness = new Vector2(1.5f, 0),
|
|
RelativeSizeAxes = Axes.Both,
|
|
Colour = accentColour,
|
|
}
|
|
};
|
|
|
|
protected override void LoadComplete()
|
|
{
|
|
base.LoadComplete();
|
|
|
|
background.Shear = new Vector2(ButtonSystem.WEDGE_WIDTH / initialSize.Y, 0);
|
|
|
|
// for whatever reason, attempting to size the background "just in time" to cover the visible width
|
|
// results in gaps when the width changes are quick (only visible when testing menu at 100% speed, not visible slowed down).
|
|
// to ensure there's no missing backdrop, just use a ballpark that should be enough to always cover the width and then some.
|
|
// note that while on a code inspections it would seem that `1.5 * initialSize.X` would be enough, elastic usings are used in this button
|
|
// (which can exceed the [0;1] range during interpolation).
|
|
backgroundContent.Width = 2 * initialSize.X;
|
|
backgroundContent.Shear = -background.Shear;
|
|
|
|
animateState();
|
|
FinishTransforms(true);
|
|
}
|
|
|
|
private bool rightward;
|
|
|
|
protected override void OnNewBeat(int beatIndex, TimingControlPoint timingPoint, EffectControlPoint effectPoint, ChannelAmplitudes amplitudes)
|
|
{
|
|
base.OnNewBeat(beatIndex, timingPoint, effectPoint, amplitudes);
|
|
|
|
if (!IsHovered) return;
|
|
|
|
double duration = timingPoint.BeatLength / 2;
|
|
|
|
icon.RotateTo(rightward ? BOUNCE_ROTATION : -BOUNCE_ROTATION, duration * 2, Easing.InOutSine);
|
|
|
|
icon.Animate(
|
|
i => i.MoveToY(-10, duration, Easing.Out),
|
|
i => i.ScaleTo(HOVER_SCALE, duration, Easing.Out)
|
|
).Then(
|
|
i => i.MoveToY(0, duration, Easing.In),
|
|
i => i.ScaleTo(new Vector2(HOVER_SCALE, HOVER_SCALE * BOUNCE_COMPRESSION), duration, Easing.In)
|
|
);
|
|
|
|
rightward = !rightward;
|
|
}
|
|
|
|
protected override bool OnHover(HoverEvent e)
|
|
{
|
|
if (State != ButtonState.Expanded) return true;
|
|
|
|
double duration = TimeUntilNextBeat;
|
|
|
|
icon.ClearTransforms();
|
|
icon.RotateTo(rightward ? -BOUNCE_ROTATION : BOUNCE_ROTATION, duration, Easing.InOutSine);
|
|
icon.ScaleTo(new Vector2(HOVER_SCALE, HOVER_SCALE * BOUNCE_COMPRESSION), duration, Easing.Out);
|
|
|
|
sampleHover?.Play();
|
|
background.ResizeTo(Vector2.Multiply(initialSize, new Vector2(1.5f, 1)), 500, Easing.OutElastic);
|
|
|
|
return true;
|
|
}
|
|
|
|
protected override void OnHoverLost(HoverLostEvent e)
|
|
{
|
|
icon.ClearTransforms();
|
|
icon.RotateTo(0, 500, Easing.Out);
|
|
icon.MoveTo(Vector2.Zero, 500, Easing.Out);
|
|
icon.ScaleTo(Vector2.One, 200, Easing.Out);
|
|
|
|
if (State == ButtonState.Expanded)
|
|
background.ResizeTo(initialSize, 500, Easing.OutElastic);
|
|
}
|
|
|
|
[BackgroundDependencyLoader]
|
|
private void load(AudioManager audio)
|
|
{
|
|
sampleHover = audio.Samples.Get(@"Menu/button-hover");
|
|
sampleClick = audio.Samples.Get(!string.IsNullOrEmpty(sampleName) ? $@"Menu/{sampleName}" : @"UI/button-select");
|
|
}
|
|
|
|
protected override bool OnMouseDown(MouseDownEvent e)
|
|
{
|
|
boxHoverLayer.FadeTo(0.1f, 1000, Easing.OutQuint);
|
|
return base.OnMouseDown(e);
|
|
}
|
|
|
|
protected override void OnMouseUp(MouseUpEvent e)
|
|
{
|
|
// HORRIBLE HACK
|
|
// This is here so that on mobile, the main menu button that progresses to song select can correctly progress to song select v2 when held.
|
|
// Once the temporary solution of holding the button to access song select v2 is removed, this should be too.
|
|
// Without this, the long-press-to-right-click flow intercepts the hold and converts it to a right click which would not trigger the button
|
|
// and therefore not progress to song select.
|
|
if (e.Button == MouseButton.Right && e.CurrentState.Mouse.LastSource is ISourcedFromTouch)
|
|
trigger(e);
|
|
// END OF HORRIBLE HACK
|
|
|
|
boxHoverLayer.FadeTo(0, 1000, Easing.OutQuint);
|
|
base.OnMouseUp(e);
|
|
}
|
|
|
|
protected override bool OnClick(ClickEvent e)
|
|
{
|
|
trigger(e);
|
|
return true;
|
|
}
|
|
|
|
protected override bool OnKeyDown(KeyDownEvent e)
|
|
{
|
|
if (e.Repeat || e.ControlPressed || e.ShiftPressed || e.AltPressed || e.SuperPressed)
|
|
return false;
|
|
|
|
if (TriggerKeys.Contains(e.Key))
|
|
{
|
|
trigger(e);
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
private void trigger(UIEvent e)
|
|
{
|
|
sampleChannel = sampleClick?.GetChannel();
|
|
sampleChannel?.Play();
|
|
|
|
clickAction?.Invoke(this, e);
|
|
|
|
boxHoverLayer.ClearTransforms();
|
|
boxHoverLayer.Alpha = 0.9f;
|
|
boxHoverLayer.FadeOut(800, Easing.OutExpo);
|
|
}
|
|
|
|
public override bool HandleNonPositionalInput => state == ButtonState.Expanded;
|
|
public override bool HandlePositionalInput => state != ButtonState.Exploded && background.Width / initialSize.X >= 0.8f;
|
|
|
|
public void StopSamplePlayback() => sampleChannel?.Stop();
|
|
|
|
protected override void Update()
|
|
{
|
|
content.Alpha = Math.Clamp((background.Width / initialSize.X - 0.5f) / 0.3f, 0, 1);
|
|
base.Update();
|
|
}
|
|
|
|
public int ContractStyle;
|
|
|
|
private ButtonState state;
|
|
|
|
public ButtonState State
|
|
{
|
|
get => state;
|
|
|
|
set
|
|
{
|
|
if (state == value)
|
|
return;
|
|
|
|
state = value;
|
|
|
|
animateState();
|
|
|
|
StateChanged?.Invoke(State);
|
|
}
|
|
}
|
|
|
|
private void animateState()
|
|
{
|
|
switch (state)
|
|
{
|
|
case ButtonState.Contracted:
|
|
switch (ContractStyle)
|
|
{
|
|
default:
|
|
background.ResizeTo(Vector2.Multiply(initialSize, new Vector2(0, 1)), 500, Easing.OutExpo);
|
|
this.FadeOut(500);
|
|
break;
|
|
|
|
case 1:
|
|
background.ResizeTo(Vector2.Multiply(initialSize, new Vector2(0, 1)), 400, Easing.InSine);
|
|
this.FadeOut(800);
|
|
break;
|
|
}
|
|
|
|
break;
|
|
|
|
case ButtonState.Expanded:
|
|
const int expand_duration = 500;
|
|
background.ResizeTo(initialSize, expand_duration, Easing.OutExpo);
|
|
this.FadeIn(expand_duration / 6f);
|
|
break;
|
|
|
|
case ButtonState.Exploded:
|
|
const int explode_duration = 200;
|
|
background.ResizeTo(Vector2.Multiply(initialSize, new Vector2(2, 1)), explode_duration, Easing.OutExpo);
|
|
this.FadeOut(explode_duration / 4f * 3);
|
|
break;
|
|
}
|
|
}
|
|
|
|
private ButtonSystemState buttonSystemState;
|
|
|
|
public ButtonSystemState ButtonSystemState
|
|
{
|
|
get => buttonSystemState;
|
|
set
|
|
{
|
|
if (buttonSystemState == value)
|
|
return;
|
|
|
|
buttonSystemState = value;
|
|
UpdateState();
|
|
}
|
|
}
|
|
|
|
protected virtual void UpdateState()
|
|
{
|
|
ContractStyle = 0;
|
|
|
|
switch (ButtonSystemState)
|
|
{
|
|
case ButtonSystemState.Initial:
|
|
State = ButtonState.Contracted;
|
|
break;
|
|
|
|
case ButtonSystemState.EnteringMode:
|
|
ContractStyle = 1;
|
|
State = ButtonState.Contracted;
|
|
break;
|
|
|
|
default:
|
|
if (ButtonSystemState <= VisibleStateMax && ButtonSystemState >= VisibleStateMin)
|
|
State = ButtonState.Expanded;
|
|
else if (ButtonSystemState < VisibleStateMin)
|
|
State = ButtonState.Contracted;
|
|
else
|
|
State = ButtonState.Exploded;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
public enum ButtonState
|
|
{
|
|
Contracted,
|
|
Expanded,
|
|
Exploded
|
|
}
|
|
}
|