// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. #nullable disable using System; 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.Graphics.Sprites; using osu.Framework.Graphics.UserInterface; using osu.Framework.Input.Bindings; using osu.Framework.Input.Events; using osu.Framework.Threading; using osu.Framework.Utils; using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; using osu.Game.Input.Bindings; using osuTK; using osuTK.Graphics; namespace osu.Game.Screens.Play.HUD { public class HoldForMenuButton : FillFlowContainer { public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => true; public readonly Bindable IsPaused = new Bindable(); private HoldButton button; public Action Action { get; set; } private OsuSpriteText text; public HoldForMenuButton() { Direction = FillDirection.Horizontal; Spacing = new Vector2(20, 0); Margin = new MarginPadding(10); } [BackgroundDependencyLoader(true)] private void load(Player player) { Children = new Drawable[] { text = new OsuSpriteText { Font = OsuFont.GetFont(weight: FontWeight.Bold), Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft }, button = new HoldButton(player?.Configuration.AllowRestart == false) { HoverGained = () => text.FadeIn(500, Easing.OutQuint), HoverLost = () => text.FadeOut(500, Easing.OutQuint), IsPaused = { BindTarget = IsPaused }, Action = () => Action(), } }; AutoSizeAxes = Axes.Both; } protected override void LoadComplete() { button.HoldActivationDelay.BindValueChanged(v => { text.Text = v.NewValue > 0 ? "hold for menu" : "press for menu"; }, true); text.FadeInFromZero(500, Easing.OutQuint).Delay(1500).FadeOut(500, Easing.OutQuint); base.LoadComplete(); } private float positionalAdjust = 1; // Start at 1 to handle the case where a user never send positional input. protected override bool OnMouseMove(MouseMoveEvent e) { positionalAdjust = Vector2.Distance(e.MousePosition, button.ToSpaceOfOtherDrawable(button.DrawRectangle.Centre, Parent)) / 100; return base.OnMouseMove(e); } protected override void Update() { base.Update(); if (text.Alpha > 0 || button.Progress.Value > 0 || button.IsHovered) Alpha = 1; else { Alpha = Interpolation.ValueAt( Math.Clamp(Clock.ElapsedFrameTime, 0, 200), Alpha, Math.Clamp(1 - positionalAdjust, 0.04f, 1), 0, 200, Easing.OutQuint); } } private class HoldButton : HoldToConfirmContainer, IKeyBindingHandler { private SpriteIcon icon; private CircularProgress circularProgress; private Circle overlayCircle; public readonly Bindable IsPaused = new Bindable(); protected override bool AllowMultipleFires => true; public Action HoverGained; public Action HoverLost; private const double shake_duration = 20; private bool pendingAnimation; private ScheduledDelegate shakeOperation; public HoldButton(bool isDangerousAction) : base(isDangerousAction) { } [BackgroundDependencyLoader] private void load(OsuColour colours) { Size = new Vector2(60); Child = new CircularContainer { Masking = true, RelativeSizeAxes = Axes.Both, Children = new Drawable[] { new Box { RelativeSizeAxes = Axes.Both, Colour = colours.Gray1, Alpha = 0.5f, }, circularProgress = new CircularProgress { RelativeSizeAxes = Axes.Both, InnerRadius = 1 }, overlayCircle = new Circle { Anchor = Anchor.Centre, Origin = Anchor.Centre, RelativeSizeAxes = Axes.Both, Colour = colours.Gray1, Size = new Vector2(0.9f), }, icon = new SpriteIcon { Shadow = false, Anchor = Anchor.Centre, Origin = Anchor.Centre, Size = new Vector2(15), Icon = FontAwesome.Solid.Times }, } }; bind(); } private void bind() { ((IBindable)circularProgress.Current).BindTo(Progress); Progress.ValueChanged += progress => { icon.Scale = new Vector2(1 + (float)progress.NewValue * 0.2f); if (IsDangerousAction) { Colour = Interpolation.ValueAt(progress.NewValue, Color4.White, Color4.Red, 0, 1, Easing.OutQuint); if (progress.NewValue > 0 && progress.NewValue < 1) { shakeOperation ??= Scheduler.AddDelayed(shake, shake_duration, true); } else { Child.MoveTo(Vector2.Zero, shake_duration * 2, Easing.OutQuint); shakeOperation?.Cancel(); shakeOperation = null; } } }; } private void shake() { const float shake_magnitude = 8; Child.MoveTo(new Vector2( RNG.NextSingle(-1, 1) * (float)Progress.Value * shake_magnitude, RNG.NextSingle(-1, 1) * (float)Progress.Value * shake_magnitude ), shake_duration); } protected override void Confirm() { base.Confirm(); // temporarily unbind as to not look weird if releasing during confirm animation (can see the unwind of progress). Progress.UnbindAll(); // avoid starting a new confirm call until we finish animating. pendingAnimation = true; AbortConfirm(); overlayCircle.ScaleTo(0, 100) .Then().FadeOut().ScaleTo(1).FadeIn(500) .OnComplete(a => { icon.ScaleTo(1, 100); circularProgress.FadeOut(100).OnComplete(_ => { bind(); circularProgress.FadeIn(); pendingAnimation = false; }); }); } protected override bool OnHover(HoverEvent e) { HoverGained?.Invoke(); return true; } protected override void OnHoverLost(HoverLostEvent e) { HoverLost?.Invoke(); base.OnHoverLost(e); } public bool OnPressed(KeyBindingPressEvent e) { if (e.Repeat) return false; switch (e.Action) { case GlobalAction.Back: case GlobalAction.PauseGameplay: // in the future this behaviour will differ for replays etc. if (!pendingAnimation) BeginConfirm(); return true; } return false; } public void OnReleased(KeyBindingReleaseEvent e) { switch (e.Action) { case GlobalAction.Back: case GlobalAction.PauseGameplay: AbortConfirm(); break; } } protected override bool OnMouseDown(MouseDownEvent e) { if (!pendingAnimation && e.CurrentState.Mouse.Buttons.Count() == 1) BeginConfirm(); return true; } protected override void OnMouseUp(MouseUpEvent e) { if (!e.HasAnyButtonPressed) AbortConfirm(); } } } }