Files

495 lines
18 KiB
C#

using System;
using System.Collections.Generic;
using TMPro;
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.UI;
using XericUI.Core.Base;
using XericUI.XTable.Core;
using XericUI.XTable.Mapping;
using XericUI.XTable.Rendering.Elements;
using XericLibrary.Runtime.SuperStyleSheet;
using XericLibrary.Runtime.UIGraph;
#if UNITY_EDITOR
using UnityEditor;
#endif
namespace XericUI.XTable.Rendering.Component
{
/// <summary>
/// 表格组件——纯数据渲染器。
/// 一张全幅背景 + 按需拼装单元格文本/图片。
/// 无行列标题、无样式差异分割线——所有内容由数据定义。
/// </summary>
[ExecuteAlways]
[AddComponentMenu("Xeric UI Vessel/Table/Xeric UI Action Table", 60)]
public partial class XericUIActionTable : XericUIBehaviour
{
#region 序列化字段
[SerializeField] private XTableData m_TableData;
[SerializeField] private float[] m_RowHeights = new float[] { 40f, 30f };
[SerializeField] private float[] m_ColWidths = new float[] { 100f, 80f, 120f };
[SerializeField] private string m_DefaultStyleNamespace = "";
[SerializeField]
[Tooltip("表格全局配置资源")]
private XTableConfig m_Config;
[SerializeField] private ScrollRect m_ScrollRect;
[SerializeField]
[Tooltip("冻结行数——顶部固定不滚动的行数")]
private int m_FreezeRowCount;
[SerializeField]
[Tooltip("冻结列数——左侧固定不滚动的列数")]
private int m_FreezeColCount;
[SerializeField]
[Tooltip("在 Hierarchy 中显示动态生成的子对象(仅编辑器,不保存到场景)")]
private bool m_ChildrenVisible;
[SerializeField] private bool m_EnableSelectionAutoScroll = true;
[SerializeField, Min(1f), Tooltip("框选自动滚动的 Content 边缘阈值(像素)")]
private float m_SelectionAutoScrollEdgeThreshold = 32f;
[SerializeField, Min(1f), Tooltip("框选自动滚动的最大速度(像素/秒)")]
private float m_SelectionAutoScrollMaxSpeed = 600f;
[SerializeField, Min(1f), Tooltip("框选自动滚动的边缘加速度指数")]
private float m_SelectionAutoScrollAcceleration = 1.5f;
#endregion
#region 私有字段
[System.NonSerialized] private CellAssembler m_Assembler;
[System.NonSerialized] private XTableDirtyFlag m_DirtyFlag;
/// <summary>Viewport 下的冻结列、冻结行与左上交叉区渲染器。</summary>
[System.NonSerialized] private TableRenderer m_FrozenColumnRenderer;
[System.NonSerialized] private TableRenderer m_FrozenRowRenderer;
[System.NonSerialized] private TableRenderer m_FrozenCornerRenderer;
[System.NonSerialized] private RectTransform m_FrozenColumnsContainer;
[System.NonSerialized] private RectTransform m_FrozenRowsContainer;
[System.NonSerialized] private RectTransform m_FrozenCornerContainer;
[System.NonSerialized] private XTableSelectionHandle m_SelectionHandle;
[System.NonSerialized] private XTableSelectionOverlay m_NormalSelectionOverlay;
[System.NonSerialized] private XTableSelectionOverlay m_FrozenColumnSelectionOverlay;
[System.NonSerialized] private XTableSelectionOverlay m_FrozenRowSelectionOverlay;
[System.NonSerialized] private XTableSelectionOverlay m_FrozenCornerSelectionOverlay;
[System.NonSerialized] private bool m_SelectionDragActive;
[System.NonSerialized] private Vector2 m_LastSelectionPointer;
[System.NonSerialized] private Camera m_SelectionPointerCamera;
[System.NonSerialized] private XTableBackgroundRenderer m_BackgroundRenderer;
/// <summary>预制体对象池——按原型分组管理</summary>
[System.NonSerialized] private PrefabPrototypePool m_PrefabPool;
/// <summary>活跃预制体实例映射 (row, col) → instance,用于回调编排</summary>
[System.NonSerialized] private Dictionary<(int, int), GameObject> m_ActivePrefabMap;
/// <summary>Texture2D → Sprite 缓存,避免每帧 Sprite.Create 分配</summary>
[System.NonSerialized] private Dictionary<Texture2D, Sprite> m_SpriteCache;
/// <summary>表格四叉树——仅多样式背景分区时按需构建</summary>
[System.NonSerialized] private XTableQuadtree m_Quadtree;
[System.NonSerialized] private bool m_QuadtreeDirty = true;
[System.NonSerialized] private readonly HashSet<string> m_ActiveStyleNamespaces = new();
[System.NonSerialized] private bool m_StyleNamespaceDirty = true;
/// <summary>各渲染区域的边框线段渲染器。</summary>
[System.NonSerialized] private UILineRendererV2 m_BorderRenderer;
[System.NonSerialized] private UILineRendererV2 m_FrozenColumnBorderRenderer;
[System.NonSerialized] private UILineRendererV2 m_FrozenRowBorderRenderer;
[System.NonSerialized] private UILineRendererV2 m_FrozenCornerBorderRenderer;
[NonSerialized] private float[] m_ResolvedRowHeights;
[NonSerialized] private float[] m_ResolvedColWidths;
[NonSerialized] private Vector2 m_LastLayoutBaseSize;
private float m_TotalWidth;
private float m_TotalHeight;
/// <summary>内容偏移量(边框半宽),避免 viewport 裁切边缘边框</summary>
private float m_ContentOffset;
// ScrollRect 可见范围
private Rect m_VisibleRect;
#endregion
#region 事件
public event Action<int, int> OnCellSelected;
public event Action OnTableRefreshed;
#endregion
#region 公开属性
public XTableData TableData
{
get
{
if (m_TableData == null)
m_TableData = new XTableData();
return m_TableData;
}
set
{
m_TableData = value;
MarkDirty(TableDirtyType.DataChanged);
}
}
public XTableCellData this[int row, int col] => TableData.GetOrCreateCell(row, col);
public float[] RowHeights => m_RowHeights;
public float[] ColWidths => m_ColWidths;
public float[] ResolvedRowHeights => m_ResolvedRowHeights;
public float[] ResolvedColWidths => m_ResolvedColWidths;
/// <summary>冻结行数</summary>
public int FreezeRowCount => m_FreezeRowCount;
/// <summary>冻结列数</summary>
public int FreezeColCount => m_FreezeColCount;
public string DefaultStyleNamespace => m_DefaultStyleNamespace;
/// <summary>选中操作句柄——外部可通过此句柄控制选区,或直接操作 StartRow/StartCol 等属性</summary>
public XTableSelectionHandle SelectionHandle
{
get
{
if (m_SelectionHandle == null)
{
m_SelectionHandle = new XTableSelectionHandle();
m_SelectionHandle.OnSelectionChanged += OnHandleSelectionChanged;
}
return m_SelectionHandle;
}
}
// ── 向后兼容属性(供 Editor 脚本使用)──
public int SelectedRow => SelectionHandle.StartRow;
public int SelectedCol => SelectionHandle.StartCol;
public int SelectStartRow => SelectionHandle.StartRow;
public int SelectStartCol => SelectionHandle.StartCol;
public int SelectEndRow => SelectionHandle.EndRow;
public int SelectEndCol => SelectionHandle.EndCol;
public bool HasMultiSelection => SelectionHandle.HasMultiSelection;
/// <summary>动态子对象在 Hierarchy 中是否可见(仅编辑器有效)</summary>
public bool ChildrenVisible
{
get => m_ChildrenVisible;
set => SetChildrenVisible(value);
}
/// <summary>动态子对象挂载的父节点。
/// 若绑定了 ScrollRect 则使用其 Content,否则回退到自身的 rectTransform。</summary>
public Transform ContentTransform
{
get
{
if (m_ScrollRect != null && m_ScrollRect.content != null)
return m_ScrollRect.content;
return rectTransform;
}
}
/// <summary>表格全局配置资源</summary>
public XTableConfig Config
{
get
{
if (m_Config == null)
m_Config = XTableConfig.LoadOrDefault();
return m_Config;
}
set => m_Config = value;
}
/// <summary>单元格内容水平内边距(单侧)</summary>
private float CellPadding => Config.CellHorizontalPadding;
private void CreatePools()
{
m_Assembler = new CellAssembler(ContentTransform, m_DefaultStyleNamespace,
Config.TextPoolMaxCapacity, Config.ImagePoolMaxCapacity);
m_PrefabPool = new PrefabPrototypePool(ContentTransform, Config.PrefabPoolMaxCapacity);
}
/// <summary>单元格内容宽度扣除量</summary>
private float CellPadding2 => Config.CellHorizontalPadding * 2f;
/// <summary>判断单元格是否在多选范围内</summary>
public bool IsInSelectionRange(int row, int col)
{
return SelectionHandle.IsInRange(row, col);
}
#endregion
#region 生命周期
private static bool s_StyleSheetsPreloaded;
/// <summary>
/// 预加载 Resources 目录下所有 XericStyleSheet 资产到 StyleManager。
/// 仅首次调用生效,后续调用直接跳过。
/// </summary>
private static void PreloadAllStyleSheets()
{
if (s_StyleSheetsPreloaded) return;
s_StyleSheetsPreloaded = true;
var sheets = Resources.LoadAll<XericStyleSheet>("");
foreach (var sheet in sheets)
{
if (sheet != null)
StyleManager.LoadStyleSheet(sheet);
}
}
protected override void Awake()
{
base.Awake();
// 预加载 Resources 中所有 .xsss 样式表到 StyleManager(仅首次)
PreloadAllStyleSheets();
// 域重载后旧子对象可能残留在场景中(DontSave 不自动销毁),先清空再重建
ClearAllChildren();
if (m_Config == null)
m_Config = XTableConfig.LoadOrDefault();
if (string.IsNullOrEmpty(m_DefaultStyleNamespace))
m_DefaultStyleNamespace = Config.DefaultStyleNamespace;
if (m_TableData == null)
m_TableData = new XTableData();
// 强制 Content 的 anchor/pivot 为 (0,1),确保 anchoredPosition 语义一致。
// 若不固定,ScrollRect 可能改变 anchor 导致坐标计算错误。
if (m_ScrollRect != null && m_ScrollRect.content != null)
{
RectTransform ct = m_ScrollRect.content as RectTransform;
if (ct != null)
{
ct.anchorMin = new Vector2(0, 1);
ct.anchorMax = new Vector2(0, 1);
ct.pivot = new Vector2(0, 1);
}
}
CreatePools();
m_ActivePrefabMap = new Dictionary<(int, int), GameObject>();
UpdateResolvedCellSizes();
RecalculateTotalSize();
EnsureDefaultStyleNamespace();
CreateDefaultSampleData();
CreateBackground();
// 创建 Viewport 层渲染器(用于冻结单元格)
CreateViewportRenderer();
if (m_ScrollRect != null)
m_ScrollRect.onValueChanged.AddListener(OnScrollValueChanged);
if (!Application.isPlaying)
SubscribeEditorUpdate();
else
m_Assembler.OnStyleChangeDetected += OnPoolStyleChanged;
MarkDirty(TableDirtyType.All);
}
protected override void OnEnable()
{
base.OnEnable();
EnsureDefaultStyleNamespace();
MarkDirty(TableDirtyType.All);
}
#if UNITY_EDITOR
protected override void OnValidate()
{
base.OnValidate();
// 编辑器下属性变更 / 选中组件时即刻刷新样式
MarkDirty(TableDirtyType.All);
}
#endif
protected override void OnDisable()
{
base.OnDisable();
}
protected override void OnDestroy()
{
if (m_ScrollRect != null)
m_ScrollRect.onValueChanged.RemoveListener(OnScrollValueChanged);
UnsubscribeEditorUpdate();
if (m_Assembler != null)
m_Assembler.OnStyleChangeDetected -= OnPoolStyleChanged;
// 销毁所有动态生成的对象
if (m_Assembler != null)
m_Assembler.DestroyAll();
m_PrefabPool?.DestroyAll();
m_ActivePrefabMap?.Clear();
DestroyBorderRenderer();
DestroyBackgroundRenderer();
DestroyFrozenRenderers();
DestroySelectionOverlay(ref m_NormalSelectionOverlay);
base.OnDestroy();
}
/// <summary>是否正在同步重建中,此时 Update / EditorUpdate 应跳过以避免重入</summary>
[System.NonSerialized] private bool m_IsRebuilding;
private void Update()
{
UpdateSelectionAutoScroll();
if (m_IsRebuilding || IsCheckingUpdate || !m_DirtyFlag.IsDirty) return;
var flags = m_DirtyFlag.Flags;
// 仅滚动或选区变更 → 轻刷新(不重建边框/背景/预制体全回收)
if ((flags & ~(TableDirtyType.ScrollChanged | TableDirtyType.SelectionChanged)) == 0)
RefreshViewLight();
else
RefreshView();
}
#endregion
#region 默认示例数据
private void CreateDefaultSampleData()
{
// 仅当数据完全为空时才创建示例数据(首次使用或数据损坏后恢复)
if (m_TableData.BlockCount > 0) return;
string[,] sample = new string[2, 3]
{
{ "Name", "Age", "City" },
{ "Alice", "25", "NYC" }
};
for (int r = 0; r < 2; r++)
for (int c = 0; c < 3; c++)
{
var data = m_TableData.GetOrCreateCell(r, c);
data.Text = sample[r, c];
}
}
#endregion
#region 脏标记与滚动回调
public void MarkDirty(TableDirtyType type)
{
if (IsCheckingUpdate)
{
m_UpdateContextStack.Peek().DirtyFlags |= type;
return;
}
ApplyDirty(type);
}
private void ApplyDirty(TableDirtyType type)
{
m_DirtyFlag.Mark(type);
// 数据、布局或样式改变只使四叉树失效;仅多样式背景需要时再构建。
if ((type & (TableDirtyType.DataChanged | TableDirtyType.LayoutChanged | TableDirtyType.StyleChanged)) != 0)
m_QuadtreeDirty = true;
if ((type & (TableDirtyType.LayoutChanged | TableDirtyType.StyleChanged)) != 0)
m_StyleNamespaceDirty = true;
}
/// <summary>运行时样式变更回调:标记脏数据,下一帧重绘</summary>
private void OnPoolStyleChanged() => MarkDirty(TableDirtyType.StyleChanged);
[System.NonSerialized] private bool m_IsRefreshingFromScroll;
private void OnRectTransformDimensionsChange()
{
Vector2 layoutBaseSize = GetLayoutBaseSize();
if (layoutBaseSize != m_LastLayoutBaseSize)
{
m_LastLayoutBaseSize = layoutBaseSize;
MarkDirty(TableDirtyType.LayoutChanged);
}
}
private Vector2 GetLayoutBaseSize()
{
RectTransform baseTransform = m_ScrollRect != null && m_ScrollRect.viewport != null
? m_ScrollRect.viewport
: transform.parent as RectTransform;
if (baseTransform == null) baseTransform = rectTransform;
Vector2 size = baseTransform != null ? baseTransform.rect.size : Vector2.zero;
Vector2 selfSize = rectTransform != null ? rectTransform.rect.size : Vector2.zero;
return new Vector2(size.x > 0f ? size.x : (m_LastLayoutBaseSize.x > 0f ? m_LastLayoutBaseSize.x : selfSize.x),
size.y > 0f ? size.y : (m_LastLayoutBaseSize.y > 0f ? m_LastLayoutBaseSize.y : selfSize.y));
}
private void UpdateResolvedCellSizes()
{
Vector2 baseSize = GetLayoutBaseSize();
m_LastLayoutBaseSize = baseSize;
if (m_RowHeights == null || m_ColWidths == null)
{
m_ResolvedRowHeights = null;
m_ResolvedColWidths = null;
return;
}
if (m_ResolvedRowHeights == null || m_ResolvedRowHeights.Length != m_RowHeights.Length)
m_ResolvedRowHeights = new float[m_RowHeights.Length];
if (m_ResolvedColWidths == null || m_ResolvedColWidths.Length != m_ColWidths.Length)
m_ResolvedColWidths = new float[m_ColWidths.Length];
for (int i = 0; i < m_RowHeights.Length; i++)
m_ResolvedRowHeights[i] = XTableCoordinateUtility.ResolveSize(m_RowHeights[i], baseSize.y);
for (int i = 0; i < m_ColWidths.Length; i++)
m_ResolvedColWidths[i] = XTableCoordinateUtility.ResolveSize(m_ColWidths[i], baseSize.x);
}
private void OnScrollValueChanged(Vector2 _)
{
if (IsCheckingUpdate) return;
// 防重入:RefreshViewLight 中操作 content 可能触发 ScrollRect 再次回调
if (m_IsRefreshingFromScroll) return;
m_IsRefreshingFromScroll = true;
try
{
// 滚动时仅做轻刷新(RefreshViewLight 内部会调用 UpdateVisibleRect),
// 不重建背景、边框、不重置 content.sizeDelta、不做孤儿清理。
RefreshViewLight();
}
finally
{
m_IsRefreshingFromScroll = false;
}
}
#endregion
}
}