using System.Collections;
using System.Collections.Generic;
using Sirenix.OdinInspector;
using TMPro;
using UnityEngine;
using UnityEngine.InputSystem;
namespace SLSUtilities.Narrative.UI
{
///
/// 关键词浮动窗口管理器。
/// 检测鼠标在 TMP 文本上悬停的 link 标签,弹出关键词解释窗口。
/// 支持嵌套窗口、右键固定、点击外部关闭,以及左键关闭时阻断台词推进。
///
/// 面板的实际内容显示、定位和固定状态由 组件管理,
/// 本类仅负责悬停检测、生命周期编排和输入分发。
///
public class KeywordTooltipUI : MonoBehaviour
{
// ---------------------------------------------------------------
// 静态属性:供 LineAdvancer 查询是否需要阻断本帧输入
// ---------------------------------------------------------------
///
/// 当任意 Tooltip 窗口处于打开状态时为 true。
/// LineAdvancer 应在处理台词推进前检查此值。
///
public static bool IsBlockingDialogueInput { get; private set; }
public static KeywordTooltipUI Instance { get; private set; }
// ---------------------------------------------------------------
// Inspector 配置
// ---------------------------------------------------------------
[TitleGroup("核心引用 (Core References)", Alignment = TitleAlignments.Centered)]
[BoxGroup("核心引用 (Core References)/UI")]
[Required("需要指定 Tooltip 面板的 Prefab(必须挂载 TooltipPanel 组件)")]
[SerializeField] private GameObject tooltipPanelPrefab;
[BoxGroup("核心引用 (Core References)/UI")]
[Required("Tooltip 生成的父级容器(RectTransform)")]
[SerializeField] private RectTransform tooltipContainer;
[BoxGroup("核心引用 (Core References)/UI")]
[Tooltip("主台词文本组件,用于检测鼠标悬停的关键词链接")]
[SerializeField] private TMP_Text mainLineText;
[BoxGroup("核心引用 (Core References)/UI")]
[Tooltip("渲染 Canvas 的摄像机。Screen Space Overlay 模式下留空")]
[SerializeField] private Camera uiCamera;
[TitleGroup("行为设置 (Behavior Settings)", Alignment = TitleAlignments.Centered)]
[BoxGroup("行为设置 (Behavior Settings)/定位")]
[Tooltip("Tooltip 左下角相对于鼠标的屏幕像素偏移量")]
[SerializeField] private Vector2 tooltipOffset = new Vector2(12f, 12f);
// ---------------------------------------------------------------
// 内部状态
// ---------------------------------------------------------------
// 所有当前打开的 Tooltip 面板(包含固定的和悬停的)
private readonly List _openPanels = new List();
// 当前唯一的悬停 Tooltip(未固定,跟随鼠标)
private TooltipPanel _hoverPanel;
// 上一帧检测到的悬停关键词
private string _lastHoveredKeyword;
// 供外部(如 OptionTooltipUI)注册的额外检测文本
private readonly List _externalTexts = new List();
///
/// 当前是否有未固定的悬停面板
///
public bool HasHoverPanel => _hoverPanel != null;
// ---------------------------------------------------------------
// Unity 生命周期
// ---------------------------------------------------------------
private void Awake()
{
Instance = this;
}
private void OnDisable()
{
CloseAllTooltips();
}
private void Update()
{
HandleHoverDetection();
HandleClickInput();
}
// ---------------------------------------------------------------
// 悬停检测
// ---------------------------------------------------------------
private void HandleHoverDetection()
{
Vector2 mousePos = Mouse.current.position.ReadValue();
// 检测当前鼠标命中的关键词链接
string hoveredKeyword = DetectHoveredKeyword(mousePos);
// 如果鼠标没命中链接,但在当前 Hover 面板内,保持悬停状态不变
bool mouseInsideHoverPanel = _hoverPanel != null &&
_hoverPanel.ContainsScreenPoint(mousePos, uiCamera);
if (mouseInsideHoverPanel && hoveredKeyword == null)
{
// 鼠标从链接移到了 Tooltip 面板 → 保持显示,不移动位置
return;
}
// 悬停目标变化 → 刷新 Hover Tooltip
if (hoveredKeyword != _lastHoveredKeyword)
{
_lastHoveredKeyword = hoveredKeyword;
CloseHoverTooltip();
if (!string.IsNullOrEmpty(hoveredKeyword))
{
// 如果该关键词已经有固定窗口存在 → 不创建新的 Hover
if (!HasPinnedPanelForKeyword(hoveredKeyword))
{
var kwData = KeywordProcessor.FindByPrimaryKeyword(hoveredKeyword);
if (kwData != null)
_hoverPanel = SpawnPanel(kwData, mousePos, pinned: false);
}
}
}
// 跟随鼠标更新位置(仅对 Hover 面板)
if (_hoverPanel != null && !_hoverPanel.IsPinned)
_hoverPanel.PositionAtScreenPoint(mousePos, tooltipOffset);
}
private string DetectHoveredKeyword(Vector2 mousePos)
{
// 先检测主台词文本
string kw = DetectLinkAt(mainLineText, mousePos);
if (kw != null) return kw;
// 再检测所有已打开面板内的描述文本(支持嵌套)
foreach (var panel in _openPanels)
{
kw = DetectLinkAt(panel.DescriptionText, mousePos);
if (kw != null) return kw;
}
// 最后检测外部注册的文本(如选项文本)
foreach (var extText in _externalTexts)
{
kw = DetectLinkAt(extText, mousePos);
if (kw != null) return kw;
}
return null;
}
// ---------------------------------------------------------------
// 点击输入处理
// ---------------------------------------------------------------
private void HandleClickInput()
{
bool leftClick = Mouse.current.leftButton.wasPressedThisFrame;
bool rightClick = Mouse.current.rightButton.wasPressedThisFrame;
if (!leftClick && !rightClick) return;
if (_openPanels.Count == 0) return;
Vector2 mousePos = Mouse.current.position.ReadValue();
// 检测是否右键点击在关键词链接上 → 固定 Hover 面板
if (rightClick)
{
string clickedKeyword = DetectHoveredKeyword(mousePos);
if (!string.IsNullOrEmpty(clickedKeyword) &&
_hoverPanel != null &&
_hoverPanel.Keyword == clickedKeyword)
{
PinHoverPanel();
return;
}
}
// 检测是否点击在任意 Tooltip 面板内部 → 如果是则不处理
if (IsMouseInsideAnyPanel(mousePos)) return;
// 点击在所有 Tooltip 外部 → 关闭所有 Tooltip
CloseAllTooltips();
// 左键关闭时,本帧阻断台词推进(下一帧自动解除)
if (leftClick)
{
IsBlockingDialogueInput = true;
StartCoroutine(UnblockNextFrame());
}
}
// ---------------------------------------------------------------
// 面板生命周期
// ---------------------------------------------------------------
private TooltipPanel SpawnPanel(KeywordData data, Vector2 screenPos, bool pinned)
{
if (tooltipPanelPrefab == null || tooltipContainer == null) return null;
var panelGO = Instantiate(tooltipPanelPrefab, tooltipContainer);
var panel = panelGO.GetComponent();
if (panel == null)
{
Debug.LogError(
$"[KeywordTooltipUI] Tooltip Prefab 上缺少 TooltipPanel 组件!" +
$"请确保 Prefab '{tooltipPanelPrefab.name}' 挂载了 TooltipPanel 脚本。",
tooltipPanelPrefab);
Destroy(panelGO);
return null;
}
panel.Initialize(data, pinned);
panel.PositionAtScreenPoint(screenPos, tooltipOffset);
_openPanels.Add(panel);
IsBlockingDialogueInput = true;
return panel;
}
private void PinHoverPanel()
{
if (_hoverPanel == null) return;
_hoverPanel.Pin();
_hoverPanel = null;
_lastHoveredKeyword = null;
}
private void CloseHoverTooltip()
{
if (_hoverPanel == null) return;
_openPanels.Remove(_hoverPanel);
if (_hoverPanel.gameObject != null)
Destroy(_hoverPanel.gameObject);
_hoverPanel = null;
if (_openPanels.Count == 0)
IsBlockingDialogueInput = false;
}
private void CloseAllTooltips()
{
foreach (var panel in _openPanels)
{
if (panel != null && panel.gameObject != null)
Destroy(panel.gameObject);
}
_openPanels.Clear();
_hoverPanel = null;
_lastHoveredKeyword = null;
IsBlockingDialogueInput = false;
}
// ---------------------------------------------------------------
// 查询方法
// ---------------------------------------------------------------
///
/// 检查指定关键词是否已有固定的面板存在。
/// 用于避免为同一个关键词生成重复的 Hover 面板。
///
private bool HasPinnedPanelForKeyword(string keyword)
{
foreach (var panel in _openPanels)
{
if (panel.IsPinned && panel.Keyword == keyword)
return true;
}
return false;
}
// ---------------------------------------------------------------
// 工具方法
// ---------------------------------------------------------------
private string DetectLinkAt(TMP_Text tmpText, Vector2 screenPos)
{
if (tmpText == null) return null;
int linkIndex = TMP_TextUtilities.FindIntersectingLink(tmpText, screenPos, uiCamera);
if (linkIndex < 0) return null;
string linkId = tmpText.textInfo.linkInfo[linkIndex].GetLinkID();
return KeywordProcessor.ExtractKeywordFromLinkId(linkId);
}
private bool IsMouseInsideAnyPanel(Vector2 screenPos)
{
foreach (var panel in _openPanels)
{
if (panel != null && panel.ContainsScreenPoint(screenPos, uiCamera))
return true;
}
return false;
}
private IEnumerator UnblockNextFrame()
{
yield return null;
IsBlockingDialogueInput = false;
}
public void RegisterExternalText(TMP_Text text)
{
if (text != null && !_externalTexts.Contains(text))
_externalTexts.Add(text);
}
public void UnregisterExternalText(TMP_Text text)
{
if (text != null)
_externalTexts.Remove(text);
}
}
}