// 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 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.Utils; using osu.Game.Configuration; using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; using osu.Game.Input.Bindings; using osuTK; namespace osu.Game.Screens.Play.HUD { public class HoldForMenuButton : FillFlowContainer { public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => true; public readonly Bindable IsPaused = new Bindable(); private readonly Button button; public Action Action { set => button.Action = value; } private readonly OsuSpriteText text; public HoldForMenuButton() { Direction = FillDirection.Horizontal; Spacing = new Vector2(20, 0); Margin = new MarginPadding(10); Children = new Drawable[] { text = new OsuSpriteText { Font = OsuFont.GetFont(weight: FontWeight.Bold), Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft }, button = new Button { HoverGained = () => text.FadeIn(500, Easing.OutQuint), HoverLost = () => text.FadeOut(500, Easing.OutQuint), IsPaused = { BindTarget = IsPaused } } }; AutoSizeAxes = Axes.Both; } [Resolved] private OsuConfigManager config { get; set; } private Bindable activationDelay; protected override void LoadComplete() { activationDelay = config.GetBindable(OsuSetting.UIHoldActivationDelay); activationDelay.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; protected override bool OnMouseMove(MouseMoveEvent e) { positionalAdjust = Vector2.Distance(e.ScreenSpaceMousePosition, button.ScreenSpaceDrawQuad.Centre) / 200; return base.OnMouseMove(e); } public bool PauseOnFocusLost { set => button.PauseOnFocusLost = value; } 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 Button : 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 readonly IBindable gameActive = new Bindable(true); [BackgroundDependencyLoader] private void load(OsuColour colours, Framework.Game game) { 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(); gameActive.BindTo(game.IsActive); } protected override void LoadComplete() { base.LoadComplete(); gameActive.BindValueChanged(_ => updateActive(), true); } private void bind() { circularProgress.Current.BindTo(Progress); Progress.ValueChanged += progress => icon.Scale = new Vector2(1 + (float)progress.NewValue * 0.2f); } private bool pendingAnimation; 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); } private bool pauseOnFocusLost = true; public bool PauseOnFocusLost { set { if (pauseOnFocusLost == value) return; pauseOnFocusLost = value; if (IsLoaded) updateActive(); } } private void updateActive() { if (!pauseOnFocusLost || IsPaused.Value) return; if (gameActive.Value) AbortConfirm(); else BeginConfirm(); } public bool OnPressed(GlobalAction action) { switch (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(GlobalAction action) { switch (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(); } } } }