using UnityEditor;
using UnityEngine;
using XericLibrary.Runtime.MacroLibrary;
using XericLibrary.Runtime.SuperStyleSheet;
using XericUI.XTable.Core;
using XericUI.XTable.Mapping;
using XericUI.XTable.Rendering.Component;
namespace XericUIEditor.XTable
{
///
/// Scene View 表格编辑覆盖层——
/// 左侧行索引、顶部列索引(A..Z)、底部输入框。
/// 点击行列交叉区域选中单元格,底部输入框编辑文本,方向键导航,Shift+方向键多选。
/// 不创建任何场景对象,通过 Handles.BeginGUI/EndGUI 绘制。
///
[InitializeOnLoad]
public static class XericUIActionTableSceneEditor
{
// ─────────────────────────── 交互状态机 ───────────────────────────
private enum EditorState
{
TableNavigation, // 默认:键盘/鼠标在表格上导航与选择
SizeEditing, // 气泡输入框中编辑行高/列宽
CellEditing // 底部工具栏中编辑单元格文本内容
}
private static EditorState s_State;
private static string s_CurrentControlName; // 当前聚焦的控件名(每帧由焦点决定)
private static bool s_Enabled;
private const string ENABLED_KEY = "XericUI_TableSceneEditor_Enabled";
private static string s_EditText = "";
private static string s_SizeEditText = ""; // 气泡输入框当前编辑文本
private static int s_SizeEditTargetIndex = -1;
private static string s_LastFocusedControl = "";
// 拖拽框选状态
private static bool s_IsDragging;
private static int s_DragStartRow = -1;
private static int s_DragStartCol = -1;
private static Rect s_TableRect;
private static float s_XScale, s_YScale;
private static float[] s_RawRowHeights, s_RawColWidths;
private static float[] s_ResolvedRowHeights, s_ResolvedColWidths;
private static XericUIActionTable s_ActiveTable;
private static readonly Color s_LabelBg = new Color(0.12f, 0.12f, 0.18f, 0.9f);
private static readonly Color s_SelectedLabel = new Color(0.2f, 0.5f, 0.9f, 0.6f);
private static readonly Color s_MultiSelectLabel = new Color(0.2f, 0.5f, 0.9f, 0.35f);
private static readonly Color s_LabelText = new Color(0.75f, 0.75f, 0.8f);
private static readonly Color s_InputBg = new Color(0.08f, 0.08f, 0.14f, 0.92f);
private static readonly Color s_SelectionColor = new Color(0.18f, 0.45f, 0.85f, 0.25f);
private static readonly Color s_MultiSelectRangeColor = new Color(0.18f, 0.45f, 0.85f, 0.12f);
private static readonly Color s_BubbleBg = new Color(0.08f, 0.1f, 0.2f, 0.95f);
private static readonly Color s_BubbleBorder = new Color(0.3f, 0.5f, 0.9f, 0.8f);
private static readonly Color s_ApplyBtnColor = new Color(0.15f, 0.4f, 0.15f, 0.9f);
private static readonly Color s_AddBtnBg = new Color(0.15f, 0.3f, 0.15f, 0.8f);
private static readonly Color s_AddBtnHover = new Color(0.2f, 0.45f, 0.2f, 0.9f);
private const float ROW_H = 22f;
private const float COL_W = 36f;
private const float MARGIN = 3f;
static XericUIActionTableSceneEditor()
{
s_Enabled = EditorPrefs.GetBool(ENABLED_KEY, true);
SceneView.duringSceneGui += OnSceneGUI;
}
[MenuItem(XTableEditorOverlay.MENU_PATH, false, 200)]
private static void ToggleEnabled()
{
s_Enabled = !s_Enabled;
EditorPrefs.SetBool(ENABLED_KEY, s_Enabled);
Menu.SetChecked(XTableEditorOverlay.MENU_PATH, s_Enabled);
SceneView.RepaintAll();
}
[MenuItem(XTableEditorOverlay.MENU_PATH, true)]
private static bool ToggleEnabledValidate()
{
Menu.SetChecked(XTableEditorOverlay.MENU_PATH, s_Enabled);
return true;
}
// ═══════════════════════════════════════════════════════════════
// 状态机入口
// ═══════════════════════════════════════════════════════════════
private static void UpdateState()
{
string focused = GUI.GetNameOfFocusedControl();
// 检测焦点切换 → 种子编辑文本
if (focused != s_LastFocusedControl)
{
s_LastFocusedControl = focused;
OnFocusChanged(focused);
}
if (string.IsNullOrEmpty(focused))
s_State = EditorState.TableNavigation;
else if (focused.StartsWith("XTableColSizeEdit_") || focused.StartsWith("XTableRowSizeEdit_"))
s_State = EditorState.SizeEditing;
else if (focused == "XTableSceneEditInput")
s_State = EditorState.CellEditing;
else
s_State = EditorState.TableNavigation;
s_CurrentControlName = focused;
}
private static void OnFocusChanged(string newFocus)
{
// 气泡输入框获得焦点:种子为当前尺寸值
if (newFocus.StartsWith("XTableColSizeEdit_") && s_ActiveTable != null)
{
if (int.TryParse(newFocus.Substring("XTableColSizeEdit_".Length), out int idx))
{
s_SizeEditTargetIndex = idx;
s_SizeEditText = s_RawColWidths != null && idx < s_RawColWidths.Length
? s_RawColWidths[idx].ToString("G")
: "";
}
}
else if (newFocus.StartsWith("XTableRowSizeEdit_") && s_ActiveTable != null)
{
if (int.TryParse(newFocus.Substring("XTableRowSizeEdit_".Length), out int idx))
{
s_SizeEditTargetIndex = idx;
s_SizeEditText = s_RawRowHeights != null && idx < s_RawRowHeights.Length
? s_RawRowHeights[idx].ToString("G")
: "";
}
}
// 底部内容输入框获得焦点:种子为当前单元格内容
else if (newFocus == "XTableSceneEditInput" && s_ActiveTable != null)
{
SyncEditText(s_ActiveTable);
}
}
/// 将 s_EditText 同步为当前选中单元格的内容(选中变更时调用)
private static void SyncEditText(XericUIActionTable table)
{
int sr = table.SelectedRow;
int sc = table.SelectedCol;
if (sr >= 0 && sc >= 0)
{
var data = table.GetCellData(sr, sc);
s_EditText = (data != null) ? (data.Text ?? "") : "";
}
else
{
s_EditText = "";
}
}
// ═══════════════════════════════════════════════════════════════
// 主入口
// ═══════════════════════════════════════════════════════════════
private static void OnSceneGUI(SceneView sceneView)
{
if (!s_Enabled) return;
GameObject sel = Selection.activeGameObject;
if (sel == null) return;
XericUIActionTable table = sel.GetComponentInChildren();
if (table == null)
table = sel.GetComponentInParent();
if (table == null || !table.isActiveAndEnabled) return;
#if UNITY_EDITOR
Canvas.ForceUpdateCanvases();
#endif
// 使用 Content 的 RectTransform 进行定位计算(而非表格组件所在的 ScrollRect 根节点)
RectTransform tableRt = table.ContentTransform.RectTransform();
if (tableRt == null) tableRt = table.GetComponent();
float[] rawRh = table.RowHeights;
float[] rawCw = table.ColWidths;
float[] rh = table.ResolvedRowHeights;
float[] cw = table.ResolvedColWidths;
if (rawRh == null || rawCw == null || rh == null || cw == null ||
rawRh.Length == 0 || rawCw.Length == 0 || rh.Length == 0 || cw.Length == 0) return;
Vector3[] corners = new Vector3[4];
tableRt.GetWorldCorners(corners);
Vector2 guiTL = HandleUtility.WorldToGUIPoint(corners[1]);
Vector2 guiBR = HandleUtility.WorldToGUIPoint(corners[3]);
float tableW = guiBR.x - guiTL.x;
float tableH = guiBR.y - guiTL.y;
if (tableW <= 0 || tableH <= 0) return;
float totalW = 0, totalH = 0;
foreach (float w in cw) totalW += w;
foreach (float h in rh) totalH += h;
if (totalW <= 0 || totalH <= 0) return;
float xScale = tableW / totalW;
float yScale = tableH / totalH;
Rect tableRect = new Rect(guiTL.x, guiTL.y, tableW, tableH);
// 更新缓存
s_TableRect = tableRect;
s_XScale = xScale;
s_YScale = yScale;
s_RawRowHeights = rawRh;
s_RawColWidths = rawCw;
s_ResolvedRowHeights = rh;
s_ResolvedColWidths = cw;
s_ActiveTable = table;
Handles.BeginGUI();
// ★ 状态机:必须最先执行,后续所有逻辑依赖 s_State
UpdateState();
// Unity 2021 兼容:阻止鼠标事件穿透 overlay 到背后 UI 对象
// 原理:在 overlay 区域注册默认控件,Scene View 拣选系统检测到控件后
// 不再选中背后 GameObject(Canvas、ScrollRect 等)
if (s_State == EditorState.TableNavigation)
{
// 覆盖区域:行标签 + 列表头 + 表格主体 + 底部工具栏
Rect overlayArea = new Rect(
tableRect.x - COL_W - MARGIN, // 包含行标签
tableRect.y - ROW_H - MARGIN, // 包含列表头
tableRect.width + COL_W + MARGIN + 4, // 包含行标签
tableRect.height + ROW_H + MARGIN + 56); // 包含列表头+工具栏
if (overlayArea.Contains(Event.current.mousePosition))
{
int blockId = GUIUtility.GetControlID(FocusType.Passive);
HandleUtility.AddDefaultControl(blockId);
}
}
DrawRowLabels(rh, tableRect, xScale, yScale, table);
DrawColumnLabels(cw, tableRect, xScale, yScale, table);
DrawGridLines(rh, cw, tableRect, xScale, yScale);
DrawSelectionHighlight(rh, cw, tableRect, xScale, yScale, table);
DrawAddButtons(rh, cw, tableRect, xScale, yScale, table);
DrawSizeEditBubble(rawRh, rawCw, rh, cw, tableRect, xScale, yScale, table);
DrawInputBar(table, tableRect.x, tableRect.y + tableRect.height + MARGIN);
// 非导航状态不处理鼠标/键盘表格操作
if (s_State == EditorState.TableNavigation)
{
HandleTableMouse(table, rh, cw, tableRect, xScale, yScale);
HandleKeyboard(table, rh, cw);
}
Handles.EndGUI();
}
#region 辅助
private static int IndexFromCoord(float coord, float[] sizes)
{
float acc = 0;
for (int i = 0; i < sizes.Length; i++)
{
if (coord >= acc && coord < acc + sizes[i]) return i;
acc += sizes[i];
}
return -1;
}
private static float SumBefore(float[] arr, int count)
{
float sum = 0;
for (int i = 0; i < count && i < arr.Length; i++) sum += arr[i];
return sum;
}
private static float SumRange(float[] arr, int start, int end)
{
float sum = 0;
for (int i = start; i < end && i < arr.Length; i++) sum += arr[i];
return sum;
}
private static bool IsRowInMultiSelect(XericUIActionTable table, int row)
{
if (!table.HasMultiSelection) return false;
int minR = Mathf.Min(table.SelectStartRow, table.SelectEndRow);
int maxR = Mathf.Max(table.SelectStartRow, table.SelectEndRow);
return row >= minR && row <= maxR;
}
private static bool IsColInMultiSelect(XericUIActionTable table, int col)
{
if (!table.HasMultiSelection) return false;
int minC = Mathf.Min(table.SelectStartCol, table.SelectEndCol);
int maxC = Mathf.Max(table.SelectStartCol, table.SelectEndCol);
return col >= minC && col <= maxC;
}
/// 创建纯色 1x1 纹理,用于按钮背景
private static Texture2D MakeTex(int w, int h, Color col)
{
Color[] pix = new Color[w * h];
for (int i = 0; i < pix.Length; i++)
pix[i] = col;
Texture2D result = new Texture2D(w, h);
result.SetPixels(pix);
result.Apply();
return result;
}
#endregion
#region 行标签
private static void DrawRowLabels(float[] rh, Rect tableRect,
float xScale, float yScale, XericUIActionTable table)
{
var style = new GUIStyle(GUI.skin.label)
{
fontSize = 10,
normal = { textColor = s_LabelText },
alignment = TextAnchor.MiddleCenter
};
float y = tableRect.y;
for (int r = 0; r < rh.Length; r++)
{
float cellH = rh[r] * yScale;
if (cellH < 2)
{
y += cellH;
continue;
}
Rect labelRect = new Rect(
tableRect.x - COL_W - MARGIN,
y,
COL_W,
cellH);
Color bgColor;
if (table.SelectedRow == r)
bgColor = s_SelectedLabel;
else if (IsRowInMultiSelect(table, r))
bgColor = s_MultiSelectLabel;
else
bgColor = s_LabelBg;
EditorGUI.DrawRect(labelRect, bgColor);
GUI.Label(labelRect, $"{r}", style);
// 表格导航状态下才响应鼠标
if (s_State != EditorState.TableNavigation)
{
y += cellH;
continue;
}
if (Event.current.type == EventType.MouseDown &&
Event.current.button == 0 &&
labelRect.Contains(Event.current.mousePosition))
{
int col = table.SelectedCol;
if (col < 0) col = 0;
s_IsDragging = true;
s_DragStartRow = r;
s_DragStartCol = col;
if (Event.current.shift && table.SelectedRow >= 0)
{
if (!table.HasMultiSelection)
table.SelectCell(table.SelectedRow, table.SelectedCol, r, col);
else
table.SelectionHandle.ExtendTo(r, col);
}
else
{
table.SelectCell(r, col);
}
table.RefreshView();
SyncEditText(table);
Event.current.Use();
SceneView.RepaintAll();
}
y += cellH;
}
}
#endregion
#region 列标签
private static void DrawColumnLabels(float[] cw, Rect tableRect,
float xScale, float yScale, XericUIActionTable table)
{
var style = new GUIStyle(GUI.skin.label)
{
fontSize = 10,
normal = { textColor = s_LabelText },
alignment = TextAnchor.MiddleCenter
};
float x = tableRect.x;
for (int c = 0; c < cw.Length; c++)
{
float cellW = cw[c] * xScale;
if (cellW < 2)
{
x += cellW;
continue;
}
Rect labelRect = new Rect(
x,
tableRect.y - ROW_H - MARGIN,
cellW,
ROW_H);
Color bgColor;
if (table.SelectedCol == c)
bgColor = s_SelectedLabel;
else if (IsColInMultiSelect(table, c))
bgColor = s_MultiSelectLabel;
else
bgColor = s_LabelBg;
EditorGUI.DrawRect(labelRect, bgColor);
string colLetter = XTableCoordinateFormat.ColumnIndexToLetter(c);
string text = cellW > 40 ? colLetter : (cellW > 18 ? colLetter : "");
GUI.Label(labelRect, text, style);
// 表格导航状态下才响应鼠标
if (s_State != EditorState.TableNavigation)
{
x += cellW;
continue;
}
if (Event.current.type == EventType.MouseDown &&
Event.current.button == 0 &&
labelRect.Contains(Event.current.mousePosition))
{
int row = table.SelectedRow;
if (row < 0) row = 0;
s_IsDragging = true;
s_DragStartRow = row;
s_DragStartCol = c;
if (Event.current.shift && table.SelectedCol >= 0)
{
if (!table.HasMultiSelection)
table.SelectCell(table.SelectedRow, table.SelectedCol, row, c);
else
table.SelectionHandle.ExtendTo(row, c);
}
else
{
table.SelectCell(row, c);
}
table.RefreshView();
SyncEditText(table);
Event.current.Use();
SceneView.RepaintAll();
}
x += cellW;
}
}
#endregion
#region 网格线
private static void DrawGridLines(float[] rh, float[] cw, Rect tr,
float xScale, float yScale)
{
Color lineColor = new Color(0.5f, 0.5f, 0.55f, 0.18f);
// 水平网格线(跳过合并区域内的线)
float y = tr.y;
for (int r = 0; r < rh.Length; r++)
{
y += rh[r] * yScale;
// 检查此行是否在合并区域内(非底部边界的行)
bool skipLine = false;
if (s_ActiveTable != null && r + 1 < rh.Length)
{
var data = s_ActiveTable.TableData;
if (data != null && data.IsCellMerged(r + 1, 0))
skipLine = true;
}
if (!skipLine)
EditorGUI.DrawRect(new Rect(tr.x, y, tr.width, 1), lineColor);
}
// 垂直网格线(跳过合并区域内的线)
float x = tr.x;
for (int c = 0; c < cw.Length; c++)
{
x += cw[c] * xScale;
// 检查此列是否在合并区域内(非右侧边界的列)
bool skipLine = false;
if (s_ActiveTable != null && c + 1 < cw.Length)
{
var data = s_ActiveTable.TableData;
if (data != null && data.IsCellMerged(0, c + 1))
skipLine = true;
}
if (!skipLine)
EditorGUI.DrawRect(new Rect(x, tr.y, 1, tr.height), lineColor);
}
}
#endregion
#region 选中高亮
private static void DrawSelectionHighlight(float[] rh, float[] cw, Rect tr,
float xScale, float yScale, XericUIActionTable table)
{
int sr = table.SelectedRow;
int sc = table.SelectedCol;
if (sr < 0 || sc < 0 || sr >= rh.Length || sc >= cw.Length) return;
// 获取选中单元格的实际显示范围(合并源需要扩展)
int hlRowSpan = 1, hlColSpan = 1;
table.IsMergeSource(sr, sc, out hlRowSpan, out hlColSpan);
if (table.HasMultiSelection)
{
int minR = Mathf.Min(table.SelectStartRow, table.SelectEndRow);
int maxR = Mathf.Max(table.SelectStartRow, table.SelectEndRow);
int minC = Mathf.Min(table.SelectStartCol, table.SelectEndCol);
int maxC = Mathf.Max(table.SelectStartCol, table.SelectEndCol);
minR = Mathf.Clamp(minR, 0, rh.Length - 1);
maxR = Mathf.Clamp(maxR, 0, rh.Length - 1);
minC = Mathf.Clamp(minC, 0, cw.Length - 1);
maxC = Mathf.Clamp(maxC, 0, cw.Length - 1);
float msx = tr.x + SumBefore(cw, minC) * xScale;
float msy = tr.y + SumBefore(rh, minR) * yScale;
float msw = SumRange(cw, minC, maxC + 1) * xScale;
float msh = SumRange(rh, minR, maxR + 1) * yScale;
EditorGUI.DrawRect(new Rect(msx + 1, msy + 1, msw - 2, msh - 2),
s_MultiSelectRangeColor);
}
// 单选高亮(合并源用完整跨度)
float sx = tr.x + SumBefore(cw, sc) * xScale;
float sy = tr.y + SumBefore(rh, sr) * yScale;
float sw = SumRange(cw, sc, sc + hlColSpan) * xScale;
float sh = SumRange(rh, sr, sr + hlRowSpan) * yScale;
EditorGUI.DrawRect(new Rect(sx + 1, sy + 1, sw - 2, sh - 2),
s_SelectionColor);
}
#endregion
#region 添加行列按钮
private static void DrawAddButtons(float[] rh, float[] cw, Rect tr,
float xScale, float yScale, XericUIActionTable table)
{
var plusStyle = new GUIStyle(GUI.skin.label)
{
fontSize = 14,
fontStyle = FontStyle.Bold,
normal = { textColor = Color.white },
alignment = TextAnchor.MiddleCenter
};
float yEnd = tr.y + tr.height;
Rect rowBtn = new Rect(tr.x - COL_W - MARGIN, yEnd + MARGIN, COL_W, ROW_H);
bool rowHover = rowBtn.Contains(Event.current.mousePosition);
EditorGUI.DrawRect(rowBtn, rowHover ? s_AddBtnHover : s_AddBtnBg);
GUI.Label(rowBtn, "+", plusStyle);
if (Event.current.type == EventType.MouseDown &&
Event.current.button == 0 && rowHover)
{
table.AddRow();
table.RefreshView();
Event.current.Use();
SceneView.RepaintAll();
}
float xEnd = tr.x + tr.width;
Rect colBtn = new Rect(xEnd + MARGIN, tr.y - ROW_H - MARGIN, COL_W, ROW_H);
bool colHover = colBtn.Contains(Event.current.mousePosition);
EditorGUI.DrawRect(colBtn, colHover ? s_AddBtnHover : s_AddBtnBg);
GUI.Label(colBtn, "+", plusStyle);
if (Event.current.type == EventType.MouseDown &&
Event.current.button == 0 && colHover)
{
table.AddColumn();
table.RefreshView();
Event.current.Use();
SceneView.RepaintAll();
}
}
#endregion
#region 气泡尺寸编辑器(含应用按钮)
private const float BUBBLE_W = 60;
private const float BUBBLE_H = 22;
private const float BUBBLE_APPLY_BTN_W = 24;
private const float BUBBLE_DEL_BTN_W = 24;
private static GUIStyle s_BubbleApplyBtnStyle;
private static GUIStyle s_BubbleApplyBtnStyleActive;
private static GUIStyle s_BubbleDelBtnStyle;
private static void EnsureBubbleStyles()
{
if (s_BubbleApplyBtnStyle != null) return;
s_BubbleApplyBtnStyle = new GUIStyle(GUI.skin.button)
{
fontSize = 11,
fontStyle = FontStyle.Bold,
normal =
{
textColor = new Color(0.5f, 0.5f, 0.5f),
background = MakeTex(1, 1, new Color(0.25f, 0.25f, 0.25f, 0.6f))
},
hover =
{
textColor = new Color(0.7f, 0.7f, 0.7f),
background = MakeTex(1, 1, new Color(0.35f, 0.35f, 0.35f, 0.8f))
},
active = { textColor = Color.white, background = MakeTex(1, 1, new Color(0.15f, 0.15f, 0.15f, 0.8f)) },
padding = new RectOffset(0, 0, 0, 0),
margin = new RectOffset(0, 0, 0, 0),
alignment = TextAnchor.MiddleCenter,
border = new RectOffset(0, 0, 0, 0)
};
s_BubbleApplyBtnStyleActive = new GUIStyle(s_BubbleApplyBtnStyle)
{
normal = { textColor = Color.white, background = MakeTex(1, 1, new Color(0.2f, 0.6f, 0.2f, 0.9f)) },
hover = { textColor = Color.white, background = MakeTex(1, 1, new Color(0.2f, 0.55f, 0.2f, 0.95f)) },
active = { textColor = Color.white, background = MakeTex(1, 1, new Color(0.1f, 0.4f, 0.1f, 0.95f)) }
};
s_BubbleDelBtnStyle = new GUIStyle(s_BubbleApplyBtnStyle)
{
normal =
{
textColor = new Color(0.6f, 0.3f, 0.3f),
background = MakeTex(1, 1, new Color(0.3f, 0.15f, 0.15f, 0.6f))
},
hover = { textColor = Color.white, background = MakeTex(1, 1, new Color(0.7f, 0.2f, 0.2f, 0.95f)) },
active = { textColor = Color.white, background = MakeTex(1, 1, new Color(0.5f, 0.1f, 0.1f, 0.95f)) }
};
}
private static void DrawSizeEditBubble(float[] rawRh, float[] rawCw, float[] rh, float[] cw, Rect tr,
float xScale, float yScale, XericUIActionTable table)
{
int selRow = table.SelectedRow;
int selCol = table.SelectedCol;
// 气泡固定总宽:输入框 + 应用按钮 + 删除按钮 + 间距
const float totalW = BUBBLE_W + BUBBLE_APPLY_BTN_W + BUBBLE_DEL_BTN_W + 4f;
// ── 列宽气泡:左对齐列左边缘,位于列标签上方 ──
if (selCol >= 0 && selCol < cw.Length && selRow < 0)
{
float cx = tr.x + SumBefore(cw, selCol) * xScale;
float bx = cx + 2f;
float by = tr.y - ROW_H - MARGIN - BUBBLE_H - 4f;
DrawBubble(table, bx, by, rawCw[selCol], totalW, isColumn: true, index: selCol);
}
// ── 行高气泡:左对齐行标签左侧 ──
if (selRow >= 0 && selRow < rh.Length && selCol < 0)
{
float ry = tr.y + SumBefore(rh, selRow) * yScale;
float rhScaled = rh[selRow] * yScale;
float bx = tr.x - COL_W - MARGIN - totalW - 4f;
float by = ry + (rhScaled - BUBBLE_H) / 2f;
DrawBubble(table, bx, by, rawRh[selRow], totalW, isColumn: false, index: selRow);
}
// ── 单元格选中时同时显示 ──
if (selRow >= 0 && selCol >= 0)
{
if (selCol < cw.Length)
{
float cx = tr.x + SumBefore(cw, selCol) * xScale;
float bx = cx + 2f;
float by = tr.y - ROW_H - MARGIN - BUBBLE_H - 4f;
DrawBubble(table, bx, by, rawCw[selCol], totalW, isColumn: true, index: selCol);
}
if (selRow < rh.Length)
{
float ry = tr.y + SumBefore(rh, selRow) * yScale;
float rhScaled = rh[selRow] * yScale;
float bx = tr.x - COL_W - MARGIN - totalW - 4f;
float by = ry + (rhScaled - BUBBLE_H) / 2f;
DrawBubble(table, bx, by, rawRh[selRow], totalW, isColumn: false, index: selRow);
}
}
}
private static void DrawBubble(XericUIActionTable table,
float x, float y, float currentValue,
float totalW, bool isColumn, int index)
{
string controlName = isColumn
? $"XTableColSizeEdit_{index}"
: $"XTableRowSizeEdit_{index}";
bool isFocused = GUI.GetNameOfFocusedControl() == controlName;
Rect bubbleRect = new Rect(x, y, totalW, BUBBLE_H);
EditorGUI.DrawRect(bubbleRect, s_BubbleBg);
EditorGUI.DrawRect(new Rect(x, y, totalW, 1), s_BubbleBorder);
EditorGUI.DrawRect(new Rect(x, y + BUBBLE_H - 1, totalW, 1), s_BubbleBorder);
EditorGUI.DrawRect(new Rect(x, y, 1, BUBBLE_H), s_BubbleBorder);
EditorGUI.DrawRect(new Rect(x + totalW - 1, y, 1, BUBBLE_H), s_BubbleBorder);
var inputStyle = new GUIStyle(GUI.skin.textField)
{
fontSize = 11,
margin = new RectOffset(1, 1, 1, 1),
alignment = TextAnchor.MiddleCenter
};
// 输入框占左
Rect inputRect = new Rect(x + 2, y + 2, BUBBLE_W - 4, BUBBLE_H - 4);
GUI.SetNextControlName(controlName);
if (isFocused)
s_SizeEditText = GUI.TextField(inputRect, s_SizeEditText, inputStyle);
else
GUI.TextField(inputRect, currentValue.ToString("F0"), inputStyle);
// 应用按钮 — 聚焦时绿色,否则灰色
EnsureBubbleStyles();
Rect applyBtnRect = new Rect(x + BUBBLE_W + 1f, y + 1, BUBBLE_APPLY_BTN_W, BUBBLE_H - 2);
var btnStyle = isFocused ? s_BubbleApplyBtnStyleActive : s_BubbleApplyBtnStyle;
if (GUI.Button(applyBtnRect, "✓", btnStyle))
{
ApplyBubbleEdit(table, isColumn, index);
GUI.FocusControl(null);
}
// 删除按钮 — 删除当前行/列
Rect delBtnRect = new Rect(x + BUBBLE_W + BUBBLE_APPLY_BTN_W + 2f, y + 1, BUBBLE_DEL_BTN_W, BUBBLE_H - 2);
if (GUI.Button(delBtnRect, "✕", s_BubbleDelBtnStyle))
{
ApplyBubbleDelete(table, isColumn, index);
}
// 回车提交
if (isFocused &&
Event.current.type == EventType.KeyDown &&
Event.current.keyCode == KeyCode.Return)
{
ApplyBubbleEdit(table, isColumn, index);
Event.current.Use();
}
}
private static void ApplyBubbleEdit(XericUIActionTable table, bool isColumn, int index)
{
string trimmed = s_SizeEditText.Trim();
if (float.TryParse(trimmed, out float value) && value > 0)
{
if (isColumn)
table.SetColWidth(index, value);
else
table.SetRowHeight(index, value);
table.RefreshView();
}
GUI.FocusControl(null);
s_State = EditorState.TableNavigation;
SceneView.RepaintAll();
}
private static void ApplyBubbleDelete(XericUIActionTable table, bool isColumn, int index)
{
if (isColumn)
table.RemoveColumn(index);
else
table.RemoveRow(index);
GUI.FocusControl(null);
s_State = EditorState.TableNavigation;
SceneView.RepaintAll();
}
#endregion
#region 底部输入框
private static void DrawInputBar(XericUIActionTable table, float x, float y)
{
const float barW = 450;
const float barH = 52;
const float rowH = 22;
const float pad = 2;
Rect barRect = new Rect(x, y, barW, barH);
EditorGUI.DrawRect(barRect, s_InputBg);
if (table.SelectedRow >= 0 && table.SelectedCol >= 0)
{
var labelStyle = new GUIStyle(GUI.skin.label)
{
fontSize = 11,
normal = { textColor = s_LabelText },
alignment = TextAnchor.MiddleCenter
};
var inputStyle = new GUIStyle(GUI.skin.textField)
{
fontSize = 12,
margin = new RectOffset(1, 1, 1, 1)
};
var btnStyle = new GUIStyle(GUI.skin.button)
{
fontSize = 11,
margin = new RectOffset(1, 1, 1, 1),
padding = new RectOffset(4, 4, 2, 2)
};
// Row 1: 坐标标签 | 输入框 | 应用按钮 + 合并/拆分按钮
float actionBtnW = 50;
float row1Y = barRect.y + pad;
float row2Y = barRect.y + rowH + pad + 4;
string cellLabel = table.GetSelectionText();
// 合并按钮(多选时显示)
bool showMerge = table.HasMultiSelection &&
(table.SelectStartRow != table.SelectEndRow ||
table.SelectStartCol != table.SelectEndCol);
// 拆分按钮(选中单元格是合并源时显示)
bool isMergeSource = table.IsMergeSource(table.SelectedRow,
table.SelectedCol, out _, out _);
float mergeBtnW = showMerge || isMergeSource ? actionBtnW : 0;
float unmergeBtnW = isMergeSource ? actionBtnW : 0;
// 坐标标签
GUI.Label(new Rect(barRect.x + 4, row1Y, 60, rowH), cellLabel, labelStyle);
// 输入框
float inputW = barW - 68 - 44 - 2 - mergeBtnW - unmergeBtnW - pad * 2 - 4;
Rect inputRect = new Rect(barRect.x + 68, row1Y, inputW, rowH);
if (s_State == EditorState.CellEditing &&
Event.current.type == EventType.KeyDown &&
Event.current.keyCode == KeyCode.Return)
{
ApplyCellEdit(table);
Event.current.Use();
}
GUI.SetNextControlName("XTableSceneEditInput");
s_EditText = GUI.TextField(inputRect, s_EditText, inputStyle);
// 应用按钮
float rightX = barRect.x + barW - pad - 44;
if (GUI.Button(new Rect(rightX, row1Y, 44, rowH), "应用", btnStyle))
{
GUI.FocusControl(null);
ApplyCellEdit(table);
}
// 合并按钮
if (showMerge)
{
var mergeStyle = new GUIStyle(btnStyle)
{
normal =
{
background = MakeTex(1, 1, new Color(0.15f, 0.3f, 0.65f, 0.8f)), textColor = Color.white
},
hover =
{
background = MakeTex(1, 1, new Color(0.2f, 0.4f, 0.85f, 0.9f)), textColor = Color.white
}
};
if (GUI.Button(new Rect(rightX - mergeBtnW - 2, row1Y, mergeBtnW, rowH), "合并", mergeStyle))
{
int minR = Mathf.Min(table.SelectStartRow, table.SelectEndRow);
int maxR = Mathf.Max(table.SelectStartRow, table.SelectEndRow);
int minC = Mathf.Min(table.SelectStartCol, table.SelectEndCol);
int maxC = Mathf.Max(table.SelectStartCol, table.SelectEndCol);
table.MergeCells(minR, minC, maxR - minR + 1, maxC - minC + 1);
table.RefreshView();
SceneView.RepaintAll();
}
}
// 拆分按钮
if (isMergeSource)
{
var unmergeStyle = new GUIStyle(btnStyle)
{
normal =
{
background = MakeTex(1, 1, new Color(0.65f, 0.25f, 0.15f, 0.8f)), textColor = Color.white
},
hover =
{
background = MakeTex(1, 1, new Color(0.85f, 0.35f, 0.2f, 0.9f)), textColor = Color.white
}
};
float unmergeX = showMerge ? rightX - mergeBtnW - unmergeBtnW - 4 : rightX - unmergeBtnW - 2;
if (GUI.Button(new Rect(unmergeX, row1Y, unmergeBtnW, rowH), "拆分", unmergeStyle))
{
table.UnmergeCells(table.SelectedRow, table.SelectedCol);
table.RefreshView();
SceneView.RepaintAll();
}
}
// Row 2: 样式命名空间下拉框
var nsLabelStyle = new GUIStyle(GUI.skin.label)
{
fontSize = 10,
normal = { textColor = new Color(0.6f, 0.6f, 0.6f) },
alignment = TextAnchor.MiddleRight
};
GUI.Label(new Rect(barRect.x + 4, row2Y, 36, rowH), "样式:", nsLabelStyle);
string currentNS = table.GetCellStyleNamespace(
table.SelectedRow, table.SelectedCol) ?? "";
var namespaces = StyleManager.GetAvailableNamespaces();
// 构建显示列表:如果当前命名空间不在可用列表中则追加到末尾
var displayNamespaces = new System.Collections.Generic.List(namespaces);
int currentIdx = displayNamespaces.IndexOf(currentNS);
if (currentIdx < 0 && !string.IsNullOrEmpty(currentNS))
{
displayNamespaces.Add(currentNS);
currentIdx = displayNamespaces.Count - 1;
}
if (currentIdx < 0) currentIdx = 0;
Rect dropRect = new Rect(barRect.x + 44, row2Y, barW - 44 - pad, rowH);
int newIdx = UnityEditor.EditorGUI.Popup(dropRect, currentIdx,
displayNamespaces.ToArray(), UnityEditor.EditorStyles.popup);
if (newIdx != currentIdx && newIdx >= 0 && newIdx < displayNamespaces.Count)
{
table.SetCellStyleNamespace(table.SelectedRow, table.SelectedCol,
displayNamespaces[newIdx]);
}
}
else
{
var hintStyle = new GUIStyle(GUI.skin.label)
{
fontSize = 10,
normal = { textColor = new Color(0.5f, 0.5f, 0.5f) },
alignment = TextAnchor.MiddleCenter
};
GUI.Label(new Rect(barRect.x, barRect.y, barW, barH / 2),
"点击左侧行号或上方列名选中单元格 | Shift+方向键多选", hintStyle);
}
}
private static void ApplyCellEdit(XericUIActionTable table)
{
if (table.SelectedRow < 0 || table.SelectedCol < 0) return;
table.SetCellText(table.SelectedRow, table.SelectedCol, s_EditText);
table.RefreshView();
s_State = EditorState.TableNavigation;
SceneView.RepaintAll();
}
#endregion
#region 表格区域鼠标(仅 TableNavigation 状态调用)
private static void HandleTableMouse(XericUIActionTable table,
float[] rh, float[] cw, Rect tr,
float xScale, float yScale)
{
Event e = Event.current;
switch (e.type)
{
case EventType.MouseDown:
if (e.button != 0) break;
if (!tr.Contains(e.mousePosition)) break;
CellFromMouse(tr, xScale, yScale, out int mdRow, out int mdCol);
if (mdRow < 0 || mdCol < 0) break;
s_IsDragging = true;
s_DragStartRow = mdRow;
s_DragStartCol = mdCol;
if (e.shift && table.SelectedRow >= 0 && table.SelectedCol >= 0)
{
if (!table.HasMultiSelection)
table.SelectCell(table.SelectedRow, table.SelectedCol, mdRow, mdCol);
else
table.SelectionHandle.ExtendTo(mdRow, mdCol);
}
else
{
table.SelectCell(mdRow, mdCol);
}
table.RefreshView();
SyncEditText(table);
e.Use();
SceneView.RepaintAll();
break;
case EventType.MouseDrag:
if (!s_IsDragging || e.button != 0) break;
CellFromMouse(tr, xScale, yScale, out int dragRow, out int dragCol);
if (dragRow < 0 || dragCol < 0) break;
table.SelectionHandle.ExtendTo(dragRow, dragCol);
table.RefreshView();
SyncEditText(table);
e.Use();
SceneView.RepaintAll();
break;
case EventType.MouseUp:
if (e.button != 0) break;
s_IsDragging = false;
break;
}
}
private static void CellFromMouse(Rect tr, float xScale, float yScale,
out int row, out int col)
{
float rx = (Event.current.mousePosition.x - tr.x) / xScale;
float ry = (Event.current.mousePosition.y - tr.y) / yScale;
col = IndexFromCoord(rx, s_ResolvedColWidths);
row = IndexFromCoord(ry, s_ResolvedRowHeights);
// 点击合并覆盖的单元格时,重定向到合并源
if (row >= 0 && col >= 0 && s_ActiveTable != null)
RedirectToMergeSource(ref row, ref col);
}
/// 如果指定单元格被合并覆盖,重定向到合并源(锚点)
private static void RedirectToMergeSource(ref int row, ref int col)
{
var data = s_ActiveTable.TableData;
if (data == null) return;
XTableCoordinateUtility.CellToBlockCoordinate(
row, col, data.BlockSizeX, data.BlockSizeY,
out int blockRow, out int blockCol,
out int localRow, out int localCol);
ulong zIndex = XTableBlockMapping.BlockCoordToZIndex(blockRow, blockCol);
if (!data.BlockMap.TryGetValue(zIndex, out XTableBlock block))
return;
var merge = block.GetMergeRedirect(localRow, localCol);
if (merge == null) return;
merge.GetRedirectTarget(out ulong targetBlockIndex, out int targetLocalRow, out int targetLocalCol);
if (merge.IsCrossBlock && data.BlockMap.TryGetValue(targetBlockIndex, out XTableBlock targetBlock))
{
// 跨块合并:从目标块的块坐标反算全局坐标
XTableBlockMapping.ZIndexToBlockCoord(targetBlockIndex,
out int targetBlockRow, out int targetBlockCol);
row = targetBlockRow * data.BlockSizeY + targetLocalRow;
col = targetBlockCol * data.BlockSizeX + targetLocalCol;
}
else
{
// 本地合并:源在同一块内,反算全局坐标
row = blockRow * data.BlockSizeY + targetLocalRow;
col = blockCol * data.BlockSizeX + targetLocalCol;
}
}
#endregion
#region 键盘(仅 TableNavigation 状态调用)
private static void HandleKeyboard(XericUIActionTable table, float[] rh, float[] cw)
{
Event e = Event.current;
if (e.type != EventType.KeyDown) return;
// Escape 清除选区
if (e.keyCode == KeyCode.Escape)
{
table.ClearSelection();
table.RefreshView();
s_EditText = "";
SyncEditText(table);
e.Use();
SceneView.RepaintAll();
return;
}
int row = table.SelectedRow;
int col = table.SelectedCol;
bool shift = e.shift;
// 无选中时,方向键自动选中第一个单元格
if (row < 0 || col < 0)
{
if (rh.Length == 0 || cw.Length == 0) return;
row = 0;
col = 0;
switch (e.keyCode)
{
case KeyCode.UpArrow:
case KeyCode.DownArrow:
case KeyCode.LeftArrow:
case KeyCode.RightArrow:
break;
default:
return;
}
table.SelectCell(row, col);
table.RefreshView();
SyncEditText(table);
e.Use();
SceneView.RepaintAll();
return;
}
switch (e.keyCode)
{
case KeyCode.UpArrow:
if (!TryClampToValid(row - 1, col, rh, cw, out row, out col)) return;
break;
case KeyCode.DownArrow:
if (!TryClampToValid(row + 1, col, rh, cw, out row, out col)) return;
break;
case KeyCode.LeftArrow:
if (!TryClampToValid(row, col - 1, rh, cw, out row, out col)) return;
break;
case KeyCode.RightArrow:
if (!TryClampToValid(row, col + 1, rh, cw, out row, out col)) return;
break;
default:
return;
}
if (shift)
{
if (!table.HasMultiSelection && table.SelectedRow >= 0 && table.SelectedCol >= 0)
table.SelectCell(table.SelectedRow, table.SelectedCol, row, col);
else
table.SelectionHandle.ExtendTo(row, col);
}
else
{
RedirectToMergeSource(ref row, ref col);
table.SelectCell(row, col);
}
table.RefreshView();
SyncEditText(table);
e.Use();
SceneView.RepaintAll();
}
private static bool TryClampToValid(int row, int col, float[] rh, float[] cw,
out int outRow, out int outCol)
{
if (row < 0 || row >= rh.Length || col < 0 || col >= cw.Length)
{
outRow = row;
outCol = col;
return false;
}
outRow = row;
outCol = col;
return true;
}
#endregion
}
}