Files
ADOFAIRenderer/UI/EscapeHandlerUI.cs
2026-10-15 18:29:22 +09:00

157 lines
6.8 KiB
C#

using System.Diagnostics;
using ADOFAIRenderer.Core;
using TMPro;
using UnityEngine;
using UnityEngine.UI;
namespace ADOFAIRenderer.UI;
/// <summary>
/// Hold-Escape-to-cancel handler shown during a render.
/// <para>
/// Created by <c>RenderManager</c> when the render reaches the OnScene stage, on a GameObject kept across scene loads.
/// While Escape is held, a centered panel shows a countdown from 3 seconds; holding it for the full 3 seconds
/// (measured in real time with a <see cref="Stopwatch"/>, independent of the patched game time) calls
/// <c>RenderManager.CancelRender</c>. Releasing Escape earlier hides the panel and resets the countdown.
/// The component destroys its GameObject as soon as <c>RenderManager.IsActive</c> is false (stage below OnScene).
/// Runs on the main Unity thread.
/// </para>
/// </summary>
internal class EscapeHandlerUI : MonoBehaviour
{
/// <summary>Centered countdown panel (background and text); inactive while Escape is not held.</summary>
private GameObject _containerObj = null!;
/// <summary>Countdown text inside <see cref="_containerObj"/>.</summary>
private TextMeshProUGUI _textComponent = null!;
/// <summary>Real-time hold duration of the current Escape press; <c>null</c> while Escape is not held.</summary>
private Stopwatch? _escapeStopwatch;
/// <summary>
/// Builds the overlay canvas and the hidden countdown panel. Layout values are canvas units at the 1920x1080
/// reference resolution.
/// </summary>
private void Start()
{
// Set UI layer (5, Unity's built-in "UI" layer)
gameObject.layer = 5;
// Canvas setup
var canvas = gameObject.AddComponent<Canvas>();
canvas.renderMode = RenderMode.ScreenSpaceOverlay;
canvas.sortingOrder = 10000; // Placed above debug monitor
// Canvas Scaler: 1920x1080 reference resolution, width and height matched equally (0.5)
var scaler = gameObject.AddComponent<CanvasScaler>();
scaler.uiScaleMode = CanvasScaler.ScaleMode.ScaleWithScreenSize;
scaler.referenceResolution = new Vector2(1920, 1080);
scaler.screenMatchMode = CanvasScaler.ScreenMatchMode.MatchWidthOrHeight;
scaler.matchWidthOrHeight = 0.5f;
gameObject.AddComponent<GraphicRaycaster>();
// 1. Create container and background panel object: 460x160 units at the screen center
_containerObj = new GameObject("EscapeContainer");
_containerObj.transform.SetParent(transform, false);
_containerObj.layer = 5;
var bgImage = _containerObj.AddComponent<Image>();
bgImage.color = new Color(0.05f, 0.05f, 0.07f, 0.85f); // Dark tone acrylic style
var containerRect = _containerObj.GetComponent<RectTransform>();
containerRect.anchorMin = new Vector2(0.5f, 0.5f);
containerRect.anchorMax = new Vector2(0.5f, 0.5f);
containerRect.pivot = new Vector2(0.5f, 0.5f);
containerRect.anchoredPosition = Vector2.zero; // Screen center
containerRect.sizeDelta = new Vector2(460, 160);
// 2. Create text object: 60 pt centered rich text, no wrapping, 10-unit margin inside the panel
var textObj = new GameObject("EscapeText");
textObj.transform.SetParent(_containerObj.transform, false);
textObj.layer = 5;
_textComponent = textObj.AddComponent<TextMeshProUGUI>();
_textComponent.alignment = TextAlignmentOptions.Center;
_textComponent.fontSize = 60;
_textComponent.textWrappingMode = TextWrappingModes.NoWrap;
_textComponent.richText = true;
// Apply the game's TextMeshPro font for the current language (RDString.fontData.fontTMP, assigned in
// RDString.Setup). It is null before RDString.Setup has run; TextMeshPro's default font is kept then.
if (RDString.fontData.fontTMP != null)
_textComponent.font = RDString.fontData.fontTMP;
var textRect = textObj.GetComponent<RectTransform>();
textRect.anchorMin = Vector2.zero;
textRect.anchorMax = Vector2.one;
textRect.offsetMin = new Vector2(10, 10);
textRect.offsetMax = new Vector2(-10, -10);
// Initial state disabled
_containerObj.SetActive(false);
}
/// <summary>
/// Tracks the Escape key every frame: shows and updates the countdown while it is held, cancels the render after
/// 3 seconds of holding, and hides the panel when it is released.
/// </summary>
private void Update()
{
// Destroy self once the render is inactive (stage below OnScene: finished, cancelled or failed)
if (!RenderManager.IsActive)
{
Destroy(gameObject);
return;
}
if (Input.GetKey(KeyCode.Escape))
{
// First frame of the press: start timing and show the panel
if (_escapeStopwatch == null)
{
_escapeStopwatch = new Stopwatch();
_escapeStopwatch.Start();
_containerObj.SetActive(true);
}
var elapsed = (float)_escapeStopwatch.Elapsed.TotalSeconds;
var remaining = Mathf.Max(0f, 3f - elapsed);
// Update remaining time text (seconds, one decimal). The text is fixed English, not localized.
_textComponent.text = $"<color=#FF5252>Cancelling in {remaining:F1}s..</color>\n<size=40><color=#DDDDDD>Hold Esc to cancel render</color></size>";
// Grow animation: the panel scales linearly from 1.0 to 1.15 over the 3-second hold
var scale = 1.0f + elapsed / 3.0f * 0.15f;
_containerObj.transform.localScale = new Vector3(scale, scale, 1f);
// Held for the full 3 seconds: reset the panel, then cancel
if (elapsed < 3f) return;
_escapeStopwatch.Stop();
_escapeStopwatch = null;
_containerObj.transform.localScale = Vector3.one; // Reset scale
_containerObj.SetActive(false);
// Cancel rendering: RenderManager.CancelRender resets the render state, stops the encoder as cancelled and
// ends the render. StopFFmpeg also destroys this handler's GameObject.
if (RenderManager.Instance)
RenderManager.Instance.CancelRender();
}
else
{
// Escape released before 3 seconds: stop timing and hide the panel
if (_escapeStopwatch == null) return;
_escapeStopwatch.Stop();
_escapeStopwatch = null;
_containerObj.transform.localScale = Vector3.one; // Reset scale
_containerObj.SetActive(false);
}
}
/// <summary>Stops the hold stopwatch if a press is in progress.</summary>
private void OnDestroy()
{
if (_escapeStopwatch == null) return;
_escapeStopwatch.Stop();
_escapeStopwatch = null;
}
}