diff --git a/Editor/OverrideUI/XericStyleButtonEditor.cs b/Editor/OverrideUI/XericStyleButtonEditor.cs new file mode 100644 index 0000000..1fcef77 --- /dev/null +++ b/Editor/OverrideUI/XericStyleButtonEditor.cs @@ -0,0 +1,85 @@ +using UnityEditor; +using UnityEditor.UI; + +using UnityEngine; +using UnityEngine.UI; + +using XericUI.OverrideUI; + +namespace XericUIEditor.OverrideUI +{ + /// + /// XericStyleButton 的 Inspector 编辑器。 + /// 完全替换 Selectable 的 Transition/ColorBlock/SpriteState 区域为 StyleSheet 状态块, + /// 保留 Interactable、Navigation、onClick 等基础功能。 + /// + [CustomEditor(typeof(XericStyleButton), true)] + [CanEditMultipleObjects] + internal class XericStyleButtonEditor : SelectableEditor + { + private SerializedProperty m_InteractableProp; + private SerializedProperty m_TargetGraphicProp; + private SerializedProperty m_NavigationProp; + + private SerializedProperty m_ColorStateProp; + private SerializedProperty m_TextureStateProp; + private SerializedProperty m_EnableStyleTransitionProp; + private SerializedProperty m_OnClickProp; + + protected override void OnEnable() + { + base.OnEnable(); + + m_InteractableProp = serializedObject.FindProperty("m_Interactable"); + m_TargetGraphicProp = serializedObject.FindProperty("m_TargetGraphic"); + m_NavigationProp = serializedObject.FindProperty("m_Navigation"); + + m_ColorStateProp = serializedObject.FindProperty("m_ColorState"); + m_TextureStateProp = serializedObject.FindProperty("m_TextureState"); + m_EnableStyleTransitionProp = serializedObject.FindProperty("m_EnableStyleTransition"); + m_OnClickProp = serializedObject.FindProperty("m_OnClick"); + } + + public override void OnInspectorGUI() + { + serializedObject.Update(); + + // === 1. Interactable === + EditorGUILayout.PropertyField(m_InteractableProp); + + // === 2. 样式表状态块(替代 Transition + ColorBlock + SpriteState) === + EditorGUILayout.Space(); + EditorGUILayout.LabelField("样式表配置(替代 Transition)", EditorStyles.boldLabel); + + EditorGUILayout.PropertyField(m_ColorStateProp, new GUIContent("颜色状态块"), true); + EditorGUILayout.Space(4); + EditorGUILayout.PropertyField(m_TextureStateProp, new GUIContent("纹理状态块"), true); + + if (m_ColorStateProp.isExpanded || m_TextureStateProp.isExpanded) + { + EditorGUILayout.Space(2); + EditorGUILayout.PropertyField(m_EnableStyleTransitionProp); + } + + // === 3. Target Graphic === + EditorGUILayout.Space(); + EditorGUILayout.PropertyField(m_TargetGraphicProp); + + Graphic graphic = m_TargetGraphicProp.objectReferenceValue as Graphic; + if (graphic == null) + graphic = (target as Selectable)?.GetComponent(); + if (graphic == null) + EditorGUILayout.HelpBox("你需要指定一个 Graphic 目标才能使用样式表颜色过渡。", MessageType.Warning); + + // === 4. Navigation === + EditorGUILayout.Space(); + EditorGUILayout.PropertyField(m_NavigationProp); + + // === 5. onClick === + EditorGUILayout.Space(); + EditorGUILayout.PropertyField(m_OnClickProp); + + serializedObject.ApplyModifiedProperties(); + } + } +} diff --git a/Editor/OverrideUI/XericStyleButtonEditor.cs.meta b/Editor/OverrideUI/XericStyleButtonEditor.cs.meta new file mode 100644 index 0000000..bcfb760 --- /dev/null +++ b/Editor/OverrideUI/XericStyleButtonEditor.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 93a0ee7d0089fb141a33b3db631b645a +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Editor/OverrideUI/XericStyleImageEditor.cs b/Editor/OverrideUI/XericStyleImageEditor.cs new file mode 100644 index 0000000..f345d08 --- /dev/null +++ b/Editor/OverrideUI/XericStyleImageEditor.cs @@ -0,0 +1,47 @@ +using UnityEditor; +using UnityEditor.UI; + +using XericUI.OverrideUI; + +namespace XericUIEditor.OverrideUI +{ + /// + /// XericStyleImage 的 Inspector 编辑器——继承 GraphicEditor, + /// 额外展示颜色、纹理、材质三个 StyleField 和自动刷新开关。 + /// + [CustomEditor(typeof(XericStyleImage), true)] + [CanEditMultipleObjects] + internal class XericStyleImageEditor : GraphicEditor + { + private SerializedProperty m_ColorStyleProp; + private SerializedProperty m_TextureStyleProp; + private SerializedProperty m_MaterialStyleProp; + private SerializedProperty m_AutoRefreshProp; + + protected override void OnEnable() + { + base.OnEnable(); + + m_ColorStyleProp = serializedObject.FindProperty("m_ColorStyle"); + m_TextureStyleProp = serializedObject.FindProperty("m_TextureStyle"); + m_MaterialStyleProp = serializedObject.FindProperty("m_MaterialStyle"); + m_AutoRefreshProp = serializedObject.FindProperty("m_AutoRefresh"); + } + + public override void OnInspectorGUI() + { + base.OnInspectorGUI(); + EditorGUILayout.Space(); + + serializedObject.Update(); + + EditorGUILayout.LabelField("样式表配置", EditorStyles.boldLabel); + EditorGUILayout.PropertyField(m_ColorStyleProp); + EditorGUILayout.PropertyField(m_TextureStyleProp); + EditorGUILayout.PropertyField(m_MaterialStyleProp); + EditorGUILayout.PropertyField(m_AutoRefreshProp); + + serializedObject.ApplyModifiedProperties(); + } + } +} diff --git a/Editor/OverrideUI/XericStyleImageEditor.cs.meta b/Editor/OverrideUI/XericStyleImageEditor.cs.meta new file mode 100644 index 0000000..7893ef6 --- /dev/null +++ b/Editor/OverrideUI/XericStyleImageEditor.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 8f9db8103c790b3449487b618b5a5db7 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Editor/OverrideUI/XericStyleToggleEditor.cs b/Editor/OverrideUI/XericStyleToggleEditor.cs new file mode 100644 index 0000000..74d8268 --- /dev/null +++ b/Editor/OverrideUI/XericStyleToggleEditor.cs @@ -0,0 +1,135 @@ +using UnityEditor; +using UnityEditor.SceneManagement; +using UnityEditor.UI; + +using UnityEngine; + +using UnityEngine.UI; + +using XericUI.OverrideUI; + +namespace XericUIEditor.OverrideUI +{ + /// + /// XericStyleToggle 的 Inspector 编辑器。 + /// 完全替换 Selectable 的 Transition/ColorBlock/SpriteState 区域为 StyleSheet 状态块, + /// 保留 Interactable、Navigation、isOn、graphic、group、onValueChanged 等基础功能。 + /// + [CustomEditor(typeof(XericStyleToggle), true)] + [CanEditMultipleObjects] + internal class XericStyleToggleEditor : SelectableEditor + { + // Selectable 基础 + private SerializedProperty m_InteractableProp; + private SerializedProperty m_NavigationProp; + + // Toggle 基础 + private SerializedProperty m_IsOnProp; + private SerializedProperty m_TransitionProp; + private SerializedProperty m_GraphicProp; + private SerializedProperty m_GroupProp; + private SerializedProperty m_OnValueChangedProp; + + // StyleSheet 专用 + private SerializedProperty m_ColorStateProp; + private SerializedProperty m_TextureStateProp; + private SerializedProperty m_OnOverrideColorProp; + private SerializedProperty m_OnOverrideTextureProp; + private SerializedProperty m_CheckmarkStyleProp; + private SerializedProperty m_EnableStyleTransitionProp; + + protected override void OnEnable() + { + base.OnEnable(); + + m_InteractableProp = serializedObject.FindProperty("m_Interactable"); + m_NavigationProp = serializedObject.FindProperty("m_Navigation"); + + m_IsOnProp = serializedObject.FindProperty("m_IsOn"); + m_TransitionProp = serializedObject.FindProperty("toggleTransition"); + m_GraphicProp = serializedObject.FindProperty("graphic"); + m_GroupProp = serializedObject.FindProperty("m_Group"); + m_OnValueChangedProp = serializedObject.FindProperty("onValueChanged"); + + m_ColorStateProp = serializedObject.FindProperty("m_ColorState"); + m_TextureStateProp = serializedObject.FindProperty("m_TextureState"); + m_OnOverrideColorProp = serializedObject.FindProperty("m_OnOverrideColor"); + m_OnOverrideTextureProp = serializedObject.FindProperty("m_OnOverrideTexture"); + m_CheckmarkStyleProp = serializedObject.FindProperty("m_CheckmarkStyle"); + m_EnableStyleTransitionProp = serializedObject.FindProperty("m_EnableStyleTransition"); + } + + public override void OnInspectorGUI() + { + serializedObject.Update(); + XericStyleToggle toggle = target as XericStyleToggle; + + // === 1. Interactable === + EditorGUILayout.PropertyField(m_InteractableProp); + + // === 2. 样式表状态块(替代 Transition + ColorBlock + SpriteState) === + EditorGUILayout.Space(); + EditorGUILayout.LabelField("样式表配置(替代 Transition)", EditorStyles.boldLabel); + + EditorGUILayout.PropertyField(m_ColorStateProp, new GUIContent("颜色状态块"), true); + EditorGUILayout.Space(4); + EditorGUILayout.PropertyField(m_TextureStateProp, new GUIContent("纹理状态块"), true); + + bool anyExpanded = m_ColorStateProp.isExpanded || m_TextureStateProp.isExpanded; + if (anyExpanded) + { + EditorGUILayout.Space(2); + EditorGUILayout.PropertyField(m_OnOverrideColorProp); + EditorGUILayout.PropertyField(m_OnOverrideTextureProp); + EditorGUILayout.PropertyField(m_CheckmarkStyleProp); + EditorGUILayout.PropertyField(m_EnableStyleTransitionProp); + } + + // === 3. isOn === + EditorGUILayout.Space(); + EditorGUI.BeginChangeCheck(); + EditorGUILayout.PropertyField(m_IsOnProp); + if (EditorGUI.EndChangeCheck()) + { + if (!Application.isPlaying) + EditorSceneManager.MarkSceneDirty(toggle!.gameObject.scene); + + ToggleGroup group = m_GroupProp.objectReferenceValue as ToggleGroup; + toggle!.isOn = m_IsOnProp.boolValue; + + if (group != null && group.isActiveAndEnabled && toggle!.IsActive()) + { + if (toggle!.isOn || (!group.AnyTogglesOn() && !group.allowSwitchOff)) + { + toggle!.isOn = true; + group.NotifyToggleOn(toggle!); + } + } + } + + // === 4. Toggle 切换效果 & Graphic === + EditorGUILayout.PropertyField(m_TransitionProp); + EditorGUILayout.PropertyField(m_GraphicProp); + + // === 5. Group === + EditorGUI.BeginChangeCheck(); + EditorGUILayout.PropertyField(m_GroupProp); + if (EditorGUI.EndChangeCheck()) + { + if (!Application.isPlaying) + EditorSceneManager.MarkSceneDirty(toggle!.gameObject.scene); + toggle!.group = m_GroupProp.objectReferenceValue as ToggleGroup; + } + + // === 6. Navigation === + EditorGUILayout.Space(); + EditorGUILayout.PropertyField(m_NavigationProp); + + // === 7. onValueChanged === + EditorGUILayout.Space(); + EditorGUILayout.PropertyField(m_OnValueChangedProp); + + serializedObject.ApplyModifiedProperties(); + } + } +} diff --git a/Editor/OverrideUI/XericStyleToggleEditor.cs.meta b/Editor/OverrideUI/XericStyleToggleEditor.cs.meta new file mode 100644 index 0000000..2398ca3 --- /dev/null +++ b/Editor/OverrideUI/XericStyleToggleEditor.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 333577a929d25434e87d0a53360fce29 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Editor/SuperStyleSheet.meta b/Editor/SuperStyleSheet.meta new file mode 100644 index 0000000..668124b --- /dev/null +++ b/Editor/SuperStyleSheet.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 44c3980d2307e514a869459f8153cda2 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Editor/SuperStyleSheet/StyleFieldDrawer.cs b/Editor/SuperStyleSheet/StyleFieldDrawer.cs new file mode 100644 index 0000000..888e6db --- /dev/null +++ b/Editor/SuperStyleSheet/StyleFieldDrawer.cs @@ -0,0 +1,235 @@ +using UnityEditor; + +using UnityEngine; + +using XericLibrary.Runtime.SuperStyleSheet; + +namespace XericUIEditor +{ + /// + /// StyleField 的自定义 Inspector 属性绘制器。 + /// + /// 默认折叠状态:仅显示样式路径下拉框。 + /// 展开状态:显示命名空间、路径、资源值类型(有值时)、注解描述(有注解时)。 + /// 无值/无注解的行不显示。 + /// + [CustomPropertyDrawer(typeof(StyleField))] + public class StyleFieldDrawer : PropertyDrawer + { + private const float LINE_HEIGHT = 18f; + private const float PADDING = 2f; + private const float FOLD_BUTTON_WIDTH = 14f; + + /// 折叠状态(按 property path 缓存) + private static readonly System.Collections.Generic.Dictionary s_FoldoutStates + = new System.Collections.Generic.Dictionary(); + + public override float GetPropertyHeight(SerializedProperty property, GUIContent label) + { + string key = GetFoldKey(property); + bool expanded = s_FoldoutStates.TryGetValue(key, out bool f) && f; + + if (!expanded) + return LINE_HEIGHT + PADDING * 2; + + // 基础:命名空间 + 路径 = 2 行 + int rowCount = 2; + + // 有值才显示值类型行 + string ns = GetNamespace(property); + string path = property.FindPropertyRelative("StylePath").stringValue; + StyleValue val = StyleManager.GetValue(ns, path); + if (val != null && val.ValueType != null) + rowCount++; + + // 有注解才显示注解行 + var annotations = StyleManager.GetAnnotations(ns, path); + if (annotations != null && annotations.Count > 0) + rowCount++; + + return (LINE_HEIGHT + PADDING) * rowCount + PADDING; + } + + public override void OnGUI(Rect position, SerializedProperty property, GUIContent label) + { + string key = GetFoldKey(property); + bool expanded = s_FoldoutStates.TryGetValue(key, out bool fold) && fold; + + // 获取子属性 + SerializedProperty nsProp = property.FindPropertyRelative("Namespace"); + SerializedProperty pathProp = property.FindPropertyRelative("StylePath"); + + // === 标题行:折叠箭头 + 标签 + 路径下拉 === + Rect headerRect = new Rect(position.x, position.y + PADDING, position.width, LINE_HEIGHT); + + // 折叠按钮 + Rect foldRect = new Rect(headerRect.x, headerRect.y, FOLD_BUTTON_WIDTH, LINE_HEIGHT); + bool newFold = EditorGUI.Foldout(foldRect, expanded, GUIContent.none, true); + if (newFold != expanded) + { + s_FoldoutStates[key] = newFold; + expanded = newFold; + } + + // 标签 + Rect labelRect = new Rect(headerRect.x + FOLD_BUTTON_WIDTH, headerRect.y, + EditorGUIUtility.labelWidth - FOLD_BUTTON_WIDTH, LINE_HEIGHT); + EditorGUI.LabelField(labelRect, label); + + // 路径下拉框(始终可见) + Rect pathRect = new Rect(headerRect.x + EditorGUIUtility.labelWidth + 4f, headerRect.y, + headerRect.width - EditorGUIUtility.labelWidth - 8f, LINE_HEIGHT); + + string currentNs = GetNamespace(property); + + DrawPathDropdown(pathRect, pathProp, currentNs); + + if (!expanded) + return; + + // === 展开区域 === + float y = headerRect.y + LINE_HEIGHT + PADDING; + float indent = 16f; + float fieldWidth = position.width - indent; + + // --- 命名空间 --- + Rect nsLabelRect = new Rect(position.x + indent, y, 60f, LINE_HEIGHT); + Rect nsFieldRect = new Rect(position.x + indent + 64f, y, fieldWidth - 64f, LINE_HEIGHT); + EditorGUI.LabelField(nsLabelRect, "命名空间"); + DrawNamespaceDropdown(nsFieldRect, nsProp); + y += LINE_HEIGHT + PADDING; + + // --- 路径 --- + Rect pathLabelRect = new Rect(position.x + indent, y, 60f, LINE_HEIGHT); + Rect pathFieldRect = new Rect(position.x + indent + 64f, y, fieldWidth - 64f, LINE_HEIGHT); + EditorGUI.LabelField(pathLabelRect, "路径"); + DrawPathDropdown(pathFieldRect, pathProp, currentNs); + y += LINE_HEIGHT + PADDING; + + // --- 资源值类型(仅在有值时显示) --- + StyleValue currentValue = StyleManager.GetValue(currentNs, pathProp.stringValue); + if (currentValue != null && currentValue.ValueType != null) + { + Rect valLabelRect = new Rect(position.x + indent, y, 80f, LINE_HEIGHT); + Rect valFieldRect = new Rect(position.x + indent + 84f, y, fieldWidth - 84f, LINE_HEIGHT); + + EditorGUI.LabelField(valLabelRect, "值类型"); + + string typeDisplay = GetShortTypeName(currentValue.ValueType); + string valuePreview = currentValue.RawValue != null + ? currentValue.RawValue.ToString() + : ""; + + EditorGUI.LabelField(valFieldRect, $"{typeDisplay} = {valuePreview}"); + y += LINE_HEIGHT + PADDING; + } + + // --- 注解描述(仅在有注解时显示) --- + var annotations = StyleManager.GetAnnotations(currentNs, pathProp.stringValue); + if (annotations != null && annotations.Count > 0) + { + Rect annoLabelRect = new Rect(position.x + indent, y, 80f, LINE_HEIGHT); + Rect annoFieldRect = new Rect(position.x + indent + 84f, y, fieldWidth - 84f, LINE_HEIGHT); + + EditorGUI.LabelField(annoLabelRect, "注解"); + + // 优先显示 @Desc + annotations.TryGetValue("Desc", out string desc); + if (!string.IsNullOrEmpty(desc)) + EditorGUI.LabelField(annoFieldRect, desc); + else + { + foreach (var a in annotations) + { + EditorGUI.LabelField(annoFieldRect, $"@{a.Key}: {a.Value}"); + break; + } + } + } + + // 变更检测 + if (GUI.changed) + { + property.serializedObject.ApplyModifiedProperties(); + } + } + + #region 下拉框绘制 + + private void DrawNamespaceDropdown(Rect rect, SerializedProperty nsProp) + { + var namespaces = StyleManager.GetAvailableNamespaces(); + if (namespaces.Count == 0) + namespaces.Add("default"); + + int currentIndex = namespaces.IndexOf(nsProp.stringValue); + if (currentIndex < 0) currentIndex = 0; + + int newIndex = EditorGUI.Popup(rect, currentIndex, namespaces.ToArray()); + if (newIndex >= 0 && newIndex < namespaces.Count) + { + nsProp.stringValue = namespaces[newIndex]; + } + } + + private void DrawPathDropdown(Rect rect, SerializedProperty pathProp, string namespace_) + { + var paths = StyleManager.GetPathsForNamespace(namespace_); + + var displayOptions = new System.Collections.Generic.List(); + if (paths.Count == 0) + { + displayOptions.Add("(无可用路径)"); + } + else + { + foreach (var p in paths) + { + StyleValue val = StyleManager.GetValue(namespace_, p); + string typeStr = val?.ValueType != null ? GetShortTypeName(val.ValueType) : "?"; + string valStr = val?.RawValue != null ? val.RawValue.ToString() : ""; + if (valStr.Length > 20) valStr = valStr.Substring(0, 17) + "..."; + displayOptions.Add($"{p} [{typeStr}] {valStr}"); + } + } + + int currentIndex = System.Math.Max(0, paths.IndexOf(pathProp.stringValue)); + + int newIndex = EditorGUI.Popup(rect, currentIndex, displayOptions.ToArray()); + if (newIndex >= 0 && newIndex < paths.Count) + { + pathProp.stringValue = paths[newIndex]; + } + } + + #endregion + + #region 工具方法 + + private static string GetFoldKey(SerializedProperty property) + { + return $"{property.serializedObject.targetObject.GetInstanceID()}_{property.propertyPath}"; + } + + private static string GetNamespace(SerializedProperty property) + { + string ns = property.FindPropertyRelative("Namespace").stringValue; + return string.IsNullOrEmpty(ns) ? "default" : ns; + } + + private static string GetShortTypeName(System.Type type) + { + if (type == null) return "?"; + switch (type.Name) + { + case "Single": return "float"; + case "Int32": return "int"; + case "String": return "string"; + case "Boolean": return "bool"; + default: return type.Name; + } + } + + #endregion + } +} diff --git a/Editor/SuperStyleSheet/StyleFieldDrawer.cs.meta b/Editor/SuperStyleSheet/StyleFieldDrawer.cs.meta new file mode 100644 index 0000000..ce7f7a4 --- /dev/null +++ b/Editor/SuperStyleSheet/StyleFieldDrawer.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 8ee1f2c601403504faea388e1f91f07b +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Editor/XTable.meta b/Editor/XTable.meta new file mode 100644 index 0000000..c33bc99 --- /dev/null +++ b/Editor/XTable.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 4110db94896e7434a8eb8182809da2ad +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Editor/XTable/XTableEditorOverlay.cs b/Editor/XTable/XTableEditorOverlay.cs new file mode 100644 index 0000000..27b632a --- /dev/null +++ b/Editor/XTable/XTableEditorOverlay.cs @@ -0,0 +1,172 @@ +#if UNITY_EDITOR + +using UnityEditor; + +using UnityEngine; + +using XericUI.XTable.Rendering.Component; + +namespace XericUIEditor.XTable +{ + /// + /// 场景视图表格编辑覆盖工具——选中表格组件后,在世界编辑器视口中显示网格线和编辑辅助 UI。 + /// 包含合并工具、拖拽引用工具、单元格坐标提示等。 + /// + public static class XTableEditorOverlay + { + /// 是否启用覆盖层(可通过菜单开关) + private static bool s_Enabled = true; + + private const string MENU_PATH = "Xeric UI/表格编辑器覆盖层"; + + [MenuItem(MENU_PATH, false, 200)] + private static void ToggleOverlay() + { + s_Enabled = !s_Enabled; + Menu.SetChecked(MENU_PATH, s_Enabled); + SceneView.RepaintAll(); + } + + [MenuItem(MENU_PATH, true)] + private static bool ToggleOverlayValidate() + { + Menu.SetChecked(MENU_PATH, s_Enabled); + return true; + } + + /// + /// 在 Scene View 中绘制表格编辑辅助线。 + /// 调用时机:挂到 SceneView.duringSceneGui 事件上。 + /// 示例:SceneView.duringSceneGui += OnSceneGUI; + /// + public static void OnSceneGUI(SceneView sceneView) + { + if (!s_Enabled) return; + + // 获取当前选中的表格组件 + GameObject selected = Selection.activeGameObject; + if (selected == null) return; + + XericUIActionTable table = selected.GetComponent(); + if (table == null) return; + + RectTransform rect = table.GetComponent(); + if (rect == null) return; + + DrawGridOverlay(table, rect); + DrawCellInfo(table, rect, sceneView); + } + + /// 绘制网格线 + private static void DrawGridOverlay(XericUIActionTable table, RectTransform rect) + { + float[] rowHeights = table.RowHeights; + float[] colWidths = table.ColWidths; + if (rowHeights == null || colWidths == null) return; + + Vector3[] corners = new Vector3[4]; + rect.GetWorldCorners(corners); + + Vector3 topLeft = corners[1]; + float totalWidth = rect.rect.width; + float totalHeight = rect.rect.height; + + Handles.color = new Color(0.5f, 0.5f, 0.5f, 0.3f); + + // 绘制行分割线 + float accumulatedY = 0f; + for (int r = 0; r < rowHeights.Length && accumulatedY < totalHeight; r++) + { + float y = topLeft.y - accumulatedY - rowHeights[r]; + Vector3 start = new Vector3(topLeft.x, y, topLeft.z); + Vector3 end = new Vector3(topLeft.x + Mathf.Min(totalWidth, 500f), y, topLeft.z); + Handles.DrawLine(start, end); + accumulatedY += rowHeights[r]; + } + + // 绘制列分割线 + float accumulatedX = 0f; + for (int c = 0; c < colWidths.Length && accumulatedX < totalWidth; c++) + { + float x = topLeft.x + accumulatedX + colWidths[c]; + Vector3 start = new Vector3(x, topLeft.y, topLeft.z); + Vector3 end = new Vector3(x, topLeft.y - Mathf.Min(totalHeight, 500f), topLeft.z); + Handles.DrawLine(start, end); + accumulatedX += colWidths[c]; + } + } + + /// 绘制单元格坐标提示 + private static void DrawCellInfo(XericUIActionTable table, RectTransform rect, SceneView sceneView) + { + int selRow = table.SelectedRow; + int selCol = table.SelectedCol; + if (selRow < 0 || selCol < 0) return; + + float[] rowHeights = table.RowHeights; + float[] colWidths = table.ColWidths; + if (rowHeights == null || colWidths == null) return; + + // 计算选中单元格的世界位置 + float cellX = 0f; + for (int c = 0; c < selCol && c < colWidths.Length; c++) + cellX += colWidths[c]; + + float cellY = 0f; + for (int r = 0; r < selRow && r < rowHeights.Length; r++) + cellY += rowHeights[r]; + + Vector3[] corners = new Vector3[4]; + rect.GetWorldCorners(corners); + Vector3 topLeft = corners[1]; + + Vector3 cellCenter = new Vector3( + topLeft.x + cellX + (selCol < colWidths.Length ? colWidths[selCol] * 0.5f : 0), + topLeft.y - cellY - (selRow < rowHeights.Length ? rowHeights[selRow] * 0.5f : 0), + topLeft.z); + + // 绘制选中高亮框 + Handles.color = new Color(0.2f, 0.6f, 1f, 0.5f); + float cellW = selCol < colWidths.Length ? colWidths[selCol] : 50f; + float cellH = selRow < rowHeights.Length ? rowHeights[selRow] : 30f; + Vector3 cellTL = new Vector3(cellCenter.x - cellW * 0.5f, cellCenter.y + cellH * 0.5f, cellCenter.z); + Vector3 cellTR = new Vector3(cellCenter.x + cellW * 0.5f, cellCenter.y + cellH * 0.5f, cellCenter.z); + Vector3 cellBR = new Vector3(cellCenter.x + cellW * 0.5f, cellCenter.y - cellH * 0.5f, cellCenter.z); + Vector3 cellBL = new Vector3(cellCenter.x - cellW * 0.5f, cellCenter.y - cellH * 0.5f, cellCenter.z); + Handles.DrawSolidRectangleWithOutline( + new Vector3[] { cellTL, cellTR, cellBR, cellBL }, + new Color(0.2f, 0.6f, 1f, 0.2f), + new Color(0.2f, 0.6f, 1f, 0.8f)); + + // 显示单元格坐标标签 + Handles.Label(cellCenter + Vector3.up * 10f, + $"({selRow}, {selCol})", + new GUIStyle(EditorStyles.label) + { + normal = { textColor = Color.white }, + fontStyle = FontStyle.Bold + }); + + // 工具栏按钮(在 Scene View 顶部绘制) + Handles.BeginGUI(); + GUILayout.BeginArea(new Rect(10, 10, 200, 80)); + GUILayout.BeginVertical("表格编辑工具", GUI.skin.window); + + if (GUILayout.Button("合并选区")) + { + Debug.Log($"[XTable] 合并起始于 ({selRow}, {selCol})"); + } + + if (GUILayout.Button("取消合并")) + { + Debug.Log($"[XTable] 取消合并 ({selRow}, {selCol})"); + } + + GUILayout.EndVertical(); + GUILayout.EndArea(); + Handles.EndGUI(); + } + } +} + +#endif diff --git a/Editor/XTable/XTableEditorOverlay.cs.meta b/Editor/XTable/XTableEditorOverlay.cs.meta new file mode 100644 index 0000000..02a471d --- /dev/null +++ b/Editor/XTable/XTableEditorOverlay.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 220060be4d5df6a40afa23e4a7af1d64 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Editor/XTable/XericUIActionTableEditor.cs b/Editor/XTable/XericUIActionTableEditor.cs new file mode 100644 index 0000000..d75153e --- /dev/null +++ b/Editor/XTable/XericUIActionTableEditor.cs @@ -0,0 +1,334 @@ +using UnityEditor; +using UnityEngine; +using UnityEngine.UI; + +using XericUI.XTable.Rendering.Component; + +namespace XericUIEditor.XTable +{ + /// + /// XericUIActionTable 的 Inspector 编辑器—— + /// 折叠分组:[样式设定] [尺寸设定] [编辑测试] [数据浏览]。 + /// 使用 EditorGUILayout.Foldout 避免与数组 PropertyField 内部的折叠冲突。 + /// + [CustomEditor(typeof(XericUIActionTable))] + public class XericUIActionTableEditor : Editor + { + private XericUIActionTable m_Table; + + private SerializedProperty m_RowHeightsProp; + private SerializedProperty m_ColWidthsProp; + private SerializedProperty m_DefaultStyleNsProp; + private SerializedProperty m_ScrollRectProp; + + // 折叠状态 + private static bool s_StyleFoldout = true; + private static bool s_SizeFoldout = true; + private static bool s_EditTestFoldout; + private static bool s_DataBrowseFoldout; + + // 编辑测试参数 + private int m_BatchRowCount = 10; + private int m_BatchColCount = 10; + private float m_BatchCellHeight = 40f; + private float m_BatchCellWidth = 100f; + + private int m_MergeStartRow; + private int m_MergeStartCol; + private int m_MergeRowSpan = 2; + private int m_MergeColSpan = 2; + + private int m_EditRow; + private int m_EditCol; + private string m_EditText = ""; + + private void OnEnable() + { + m_Table = (XericUIActionTable)target; + + m_RowHeightsProp = serializedObject.FindProperty("m_RowHeights"); + m_ColWidthsProp = serializedObject.FindProperty("m_ColWidths"); + m_DefaultStyleNsProp = serializedObject.FindProperty("m_DefaultStyleNamespace"); + m_ScrollRectProp = serializedObject.FindProperty("m_ScrollRect"); + } + + public override void OnInspectorGUI() + { + serializedObject.Update(); + + // ===== 样式设定 ===== + s_StyleFoldout = EditorGUILayout.Foldout(s_StyleFoldout, "样式设定", true); + if (s_StyleFoldout) + { + EditorGUI.indentLevel++; + if (m_DefaultStyleNsProp != null) + EditorGUILayout.PropertyField(m_DefaultStyleNsProp, new GUIContent("默认命名空间")); + if (m_ScrollRectProp != null) + EditorGUILayout.PropertyField(m_ScrollRectProp, new GUIContent("ScrollRect")); + EditorGUI.indentLevel--; + } + + // ===== 尺寸设定 ===== + EditorGUILayout.Space(4); + s_SizeFoldout = EditorGUILayout.Foldout(s_SizeFoldout, "尺寸设定", true); + if (s_SizeFoldout) + { + EditorGUI.indentLevel++; + if (m_RowHeightsProp != null) + EditorGUILayout.PropertyField(m_RowHeightsProp, new GUIContent("行高"), true); + if (m_ColWidthsProp != null) + EditorGUILayout.PropertyField(m_ColWidthsProp, new GUIContent("列宽"), true); + EditorGUI.indentLevel--; + } + + // ===== 编辑测试 ===== + EditorGUILayout.Space(4); + s_EditTestFoldout = EditorGUILayout.Foldout(s_EditTestFoldout, "编辑测试", true); + if (s_EditTestFoldout) + { + EditorGUI.indentLevel++; + + EditorGUILayout.LabelField("批量生成尺寸", EditorStyles.miniBoldLabel); + m_BatchRowCount = EditorGUILayout.IntField("行数", m_BatchRowCount); + m_BatchColCount = EditorGUILayout.IntField("列数", m_BatchColCount); + m_BatchCellHeight = EditorGUILayout.FloatField("单元格高度", m_BatchCellHeight); + m_BatchCellWidth = EditorGUILayout.FloatField("单元格宽度", m_BatchCellWidth); + if (GUILayout.Button("应用批量尺寸", GUILayout.Height(22))) + ApplyBatchRowColSizes(); + + EditorGUILayout.Space(6); + + EditorGUILayout.LabelField("合并单元格", EditorStyles.miniBoldLabel); + m_MergeStartRow = EditorGUILayout.IntField("起始行", m_MergeStartRow); + m_MergeStartCol = EditorGUILayout.IntField("起始列", m_MergeStartCol); + m_MergeRowSpan = EditorGUILayout.IntField("行跨度", m_MergeRowSpan); + m_MergeColSpan = EditorGUILayout.IntField("列跨度", m_MergeColSpan); + EditorGUILayout.BeginHorizontal(); + if (GUILayout.Button("合并", GUILayout.Height(22))) + EditorApplication.delayCall += () => m_Table.MergeCells(m_MergeStartRow, m_MergeStartCol, m_MergeRowSpan, m_MergeColSpan); + if (GUILayout.Button("取消合并", GUILayout.Height(22))) + EditorApplication.delayCall += () => m_Table.UnmergeCells(m_MergeStartRow, m_MergeStartCol); + EditorGUILayout.EndHorizontal(); + + EditorGUILayout.Space(6); + + EditorGUILayout.LabelField("单元格编辑", EditorStyles.miniBoldLabel); + m_EditRow = EditorGUILayout.IntField("行", m_EditRow); + m_EditCol = EditorGUILayout.IntField("列", m_EditCol); + + var existing = m_Table.GetCellData(m_EditRow, m_EditCol); + if (existing != null && !string.IsNullOrEmpty(existing.Text) && string.IsNullOrEmpty(m_EditText)) + m_EditText = existing.Text; + + m_EditText = EditorGUILayout.TextField("文本内容", m_EditText); + + EditorGUILayout.BeginHorizontal(); + if (GUILayout.Button("应用文本", GUILayout.Height(22))) + EditorApplication.delayCall += () => m_Table.SetCellText(m_EditRow, m_EditCol, m_EditText); + if (GUILayout.Button("选中此格", GUILayout.Height(22))) + EditorApplication.delayCall += () => m_Table.SelectCell(m_EditRow, m_EditCol); + if (GUILayout.Button("清除选中", GUILayout.Height(22))) + EditorApplication.delayCall += () => m_Table.ClearSelection(); + EditorGUILayout.EndHorizontal(); + + EditorGUI.indentLevel--; + } + + // ===== 数据浏览 ===== + EditorGUILayout.Space(4); + s_DataBrowseFoldout = EditorGUILayout.Foldout(s_DataBrowseFoldout, "数据浏览", true); + if (s_DataBrowseFoldout) + { + EditorGUI.indentLevel++; + + if (m_Table.TableData != null) + { + EditorGUI.BeginDisabledGroup(true); + EditorGUILayout.IntField("块数量", m_Table.TableData.BlockCount); + EditorGUILayout.TextField("块尺寸", + $"{m_Table.TableData.BlockSizeX} × {m_Table.TableData.BlockSizeY}"); + EditorGUILayout.TextField("选中单元格", + m_Table.SelectedRow >= 0 ? $"({m_Table.SelectedRow}, {m_Table.SelectedCol})" : "(未选中)"); + EditorGUI.EndDisabledGroup(); + } + else + { + EditorGUILayout.HelpBox("TableData 为空——Awake() 执行后会自动创建默认实例。", MessageType.Info); + } + + EditorGUILayout.Space(4); + if (GUILayout.Button("手动刷新视图", GUILayout.Height(22))) + EditorApplication.delayCall += () => m_Table.RefreshView(); + + EditorGUI.indentLevel--; + } + + serializedObject.ApplyModifiedProperties(); + } + + private void ApplyBatchRowColSizes() + { + serializedObject.Update(); + + if (m_RowHeightsProp != null) + { + m_RowHeightsProp.arraySize = m_BatchRowCount; + for (int i = 0; i < m_BatchRowCount; i++) + m_RowHeightsProp.GetArrayElementAtIndex(i).floatValue = m_BatchCellHeight; + } + + if (m_ColWidthsProp != null) + { + m_ColWidthsProp.arraySize = m_BatchColCount; + for (int i = 0; i < m_BatchColCount; i++) + m_ColWidthsProp.GetArrayElementAtIndex(i).floatValue = m_BatchCellWidth; + } + + serializedObject.ApplyModifiedProperties(); + } + + #region Hierarchy 创建菜单 + + [MenuItem("GameObject/Xeric UI/Table/Xeric UI Action Table", false, 60)] + private static void CreateTable(MenuCommand menuCommand) + { + // 1. 创建表格内容节点 + GameObject contentGo = new GameObject("Content", + typeof(RectTransform), typeof(CanvasRenderer), typeof(XericUIActionTable)); + RectTransform contentRt = contentGo.GetComponent(); + contentRt.anchorMin = new Vector2(0, 1); + contentRt.anchorMax = new Vector2(0, 1); + contentRt.pivot = new Vector2(0, 1); + contentRt.anchoredPosition = Vector2.zero; + contentRt.sizeDelta = new Vector2(800, 600); + XericUIActionTable table = contentGo.GetComponent(); + + // 2. 创建 Viewport(Mask 裁剪区域) + GameObject viewportGo = new GameObject("Viewport", + typeof(RectTransform), typeof(Image), typeof(Mask)); + viewportGo.GetComponent().color = new Color(1, 1, 1, 0.01f); + RectTransform vpRt = viewportGo.GetComponent(); + vpRt.anchorMin = Vector2.zero; + vpRt.anchorMax = Vector2.one; + vpRt.sizeDelta = Vector2.zero; + contentRt.SetParent(vpRt, false); + + // 3. 创建水平滚动条 + GameObject hScrollbarGo = new GameObject("Scrollbar Horizontal", + typeof(RectTransform), typeof(Scrollbar), typeof(Image)); + RectTransform hBarRt = hScrollbarGo.GetComponent(); + hBarRt.anchorMin = new Vector2(0, 0); + hBarRt.anchorMax = new Vector2(1, 0); + hBarRt.pivot = new Vector2(0.5f, 0); + hBarRt.anchoredPosition = new Vector2(0, 4); + hBarRt.sizeDelta = new Vector2(-16, 20); + Image hBarImg = hScrollbarGo.GetComponent(); + hBarImg.color = new Color(0.15f, 0.15f, 0.15f, 0.5f); + Scrollbar hBar = hScrollbarGo.GetComponent(); + hBar.direction = Scrollbar.Direction.LeftToRight; + + GameObject hSlidingGo = new GameObject("Sliding Area", + typeof(RectTransform)); + hSlidingGo.transform.SetParent(hScrollbarGo.transform, false); + RectTransform hSlidingRt = hSlidingGo.GetComponent(); + hSlidingRt.anchorMin = Vector2.zero; + hSlidingRt.anchorMax = Vector2.one; + hSlidingRt.sizeDelta = new Vector2(-20, -20); + + GameObject hHandleGo = new GameObject("Handle", + typeof(RectTransform), typeof(Image)); + hHandleGo.transform.SetParent(hSlidingGo.transform, false); + RectTransform hHandleRt = hHandleGo.GetComponent(); + hHandleRt.anchorMin = Vector2.zero; + hHandleRt.anchorMax = Vector2.one; + hHandleRt.sizeDelta = new Vector2(-20, -20); + hHandleGo.GetComponent().color = new Color(0.4f, 0.4f, 0.4f, 0.8f); + hBar.handleRect = hHandleRt; + + // 4. 创建垂直滚动条 + GameObject vScrollbarGo = new GameObject("Scrollbar Vertical", + typeof(RectTransform), typeof(Scrollbar), typeof(Image)); + RectTransform vBarRt = vScrollbarGo.GetComponent(); + vBarRt.anchorMin = new Vector2(1, 0); + vBarRt.anchorMax = new Vector2(1, 1); + vBarRt.pivot = new Vector2(1, 0.5f); + vBarRt.anchoredPosition = new Vector2(-4, 0); + vBarRt.sizeDelta = new Vector2(20, -16); + Image vBarImg = vScrollbarGo.GetComponent(); + vBarImg.color = new Color(0.15f, 0.15f, 0.15f, 0.5f); + Scrollbar vBar = vScrollbarGo.GetComponent(); + vBar.direction = Scrollbar.Direction.BottomToTop; + + GameObject vSlidingGo = new GameObject("Sliding Area", + typeof(RectTransform)); + vSlidingGo.transform.SetParent(vScrollbarGo.transform, false); + RectTransform vSlidingRt = vSlidingGo.GetComponent(); + vSlidingRt.anchorMin = Vector2.zero; + vSlidingRt.anchorMax = Vector2.one; + vSlidingRt.sizeDelta = new Vector2(-20, -20); + + GameObject vHandleGo = new GameObject("Handle", + typeof(RectTransform), typeof(Image)); + vHandleGo.transform.SetParent(vSlidingGo.transform, false); + RectTransform vHandleRt = vHandleGo.GetComponent(); + vHandleRt.anchorMin = Vector2.zero; + vHandleRt.anchorMax = Vector2.one; + vHandleRt.sizeDelta = new Vector2(-20, -20); + vHandleGo.GetComponent().color = new Color(0.4f, 0.4f, 0.4f, 0.8f); + vBar.handleRect = vHandleRt; + + // 5. 创建 ScrollView 根节点 + GameObject scrollGo = new GameObject("Xeric UI Action Table", + typeof(RectTransform), typeof(ScrollRect), typeof(Image)); + RectTransform scrollRt = scrollGo.GetComponent(); + scrollRt.sizeDelta = new Vector2(800, 600); + scrollGo.GetComponent().color = new Color(0.1f, 0.1f, 0.1f, 0.3f); + + ScrollRect scrollRect = scrollGo.GetComponent(); + scrollRect.content = contentRt; + scrollRect.viewport = vpRt; + scrollRect.horizontalScrollbar = hBar; + scrollRect.verticalScrollbar = vBar; + scrollRect.horizontalScrollbarVisibility = ScrollRect.ScrollbarVisibility.AutoHide; + scrollRect.verticalScrollbarVisibility = ScrollRect.ScrollbarVisibility.AutoHide; + scrollRect.movementType = ScrollRect.MovementType.Clamped; + + // 组装层级 + vpRt.SetParent(scrollRt, false); + hScrollbarGo.transform.SetParent(scrollRt, false); + vScrollbarGo.transform.SetParent(scrollRt, false); + + // 绑定 ScrollRect 到表格(通过 SerializedObject 正确回写) + using (SerializedObject so = new SerializedObject(table)) + { + SerializedProperty prop = so.FindProperty("m_ScrollRect"); + if (prop != null) + { + prop.objectReferenceValue = scrollRect; + so.ApplyModifiedProperties(); + } + } + + // 挂载到 Canvas + GameObject parent = GetOrCreateCanvas(); + scrollRt.SetParent(parent.transform, false); + + Undo.RegisterCreatedObjectUndo(scrollGo, "Create Xeric UI Action Table"); + Selection.activeGameObject = scrollGo; + } + + private static GameObject GetOrCreateCanvas() + { + Canvas canvas = Object.FindObjectOfType(); + if (canvas != null && canvas.isRootCanvas) + return canvas.gameObject; + + GameObject canvasGo = new GameObject("Canvas", + typeof(Canvas), typeof(CanvasScaler), typeof(GraphicRaycaster)); + canvasGo.GetComponent().renderMode = RenderMode.ScreenSpaceOverlay; + return canvasGo; + } + + #endregion + } +} diff --git a/Editor/XTable/XericUIActionTableEditor.cs.meta b/Editor/XTable/XericUIActionTableEditor.cs.meta new file mode 100644 index 0000000..e7aec11 --- /dev/null +++ b/Editor/XTable/XericUIActionTableEditor.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: a9a0aa38413f5264a97d77cb6bdfde8d +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Editor/XTable/XericUIActionTableSceneEditor.cs b/Editor/XTable/XericUIActionTableSceneEditor.cs new file mode 100644 index 0000000..4dab4f4 --- /dev/null +++ b/Editor/XTable/XericUIActionTableSceneEditor.cs @@ -0,0 +1,371 @@ +using UnityEditor; + +using UnityEngine; + +using XericUI.XTable.Rendering.Component; + +namespace XericUIEditor.XTable +{ + /// + /// Scene View 表格编辑覆盖层—— + /// 左侧行索引、顶部列索引、底部输入框。 + /// 点击行列交叉区域选中单元格,底部输入框编辑文本。 + /// 不创建任何场景对象,通过 Handles.BeginGUI/EndGUI 绘制。 + /// + [InitializeOnLoad] + public static class XericUIActionTableSceneEditor + { + private static bool s_Enabled; + private const string ENABLED_KEY = "XericUI_TableSceneEditor_Enabled"; + + private static string s_EditText = ""; + + 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_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 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("Xeric UI/表格编辑器覆盖层", false, 200)] + private static void ToggleEnabled() + { + s_Enabled = !s_Enabled; + EditorPrefs.SetBool(ENABLED_KEY, s_Enabled); + Menu.SetChecked("Xeric UI/表格编辑器覆盖层", s_Enabled); + SceneView.RepaintAll(); + } + + [MenuItem("Xeric UI/表格编辑器覆盖层", true)] + private static bool ToggleEnabledValidate() + { + Menu.SetChecked("Xeric UI/表格编辑器覆盖层", s_Enabled); + return true; + } + + 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 + + RectTransform tableRt = table.GetComponent(); + float[] rh = table.RowHeights; + float[] cw = table.ColWidths; + if (rh == null || cw == null || rh.Length == 0 || cw.Length == 0) return; + + // GetWorldCorners 返回世界坐标,topleft = corners[1] + Vector3[] corners = new Vector3[4]; + tableRt.GetWorldCorners(corners); + + Camera svCam = sceneView.camera; + Rect camRect = svCam.pixelRect; + + // 对 ScreenSpaceOverlay Canvas,世界坐标即屏幕像素坐标 + // 需要映射到 SceneView 本地 GUI 坐标 + Vector2 guiTL = ScreenToSceneViewGUI(corners[1], camRect); + Vector2 guiBR = ScreenToSceneViewGUI(corners[3], camRect); + + 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; + + // 表格区域(GUI 坐标,Y 向下) + Rect tableRect = new Rect(guiTL.x, guiTL.y, tableW, tableH); + + Handles.BeginGUI(); + + 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); + DrawInputBar(table, tableRect.x, tableRect.y + tableRect.height + MARGIN); + HandleInput(table, rh, cw, tableRect, xScale, yScale); + + Handles.EndGUI(); + } + + /// 屏幕坐标转 SceneView 本地 GUI 坐标 + private static Vector2 ScreenToSceneViewGUI(Vector2 screenPos, Rect camPixelRect) + { + return new Vector2( + screenPos.x - camPixelRect.x, + camPixelRect.yMax - screenPos.y + ); + } + + #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); + + bool isSelected = (table.SelectedRow == r); + EditorGUI.DrawRect(labelRect, isSelected ? s_SelectedLabel : s_LabelBg); + GUI.Label(labelRect, $"{r}", style); + + if (Event.current.type == EventType.MouseDown && + Event.current.button == 0 && + labelRect.Contains(Event.current.mousePosition)) + { + int col = table.SelectedCol; + if (col < 0) col = 0; + table.SelectCell(r, col); + table.RefreshView(); + UpdateEditText(table, r, col); + 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); + + bool isSelected = (table.SelectedCol == c); + EditorGUI.DrawRect(labelRect, isSelected ? s_SelectedLabel : s_LabelBg); + string text = cellW > 40 ? $"列{c}" : (cellW > 18 ? $"{c}" : ""); + GUI.Label(labelRect, text, style); + + if (Event.current.type == EventType.MouseDown && + Event.current.button == 0 && + labelRect.Contains(Event.current.mousePosition)) + { + int row = table.SelectedRow; + if (row < 0) row = 0; + table.SelectCell(row, c); + table.RefreshView(); + UpdateEditText(table, row, c); + 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; + 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; + 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; + + float sx = tr.x; + for (int c = 0; c < sc; c++) sx += cw[c] * xScale; + + float sy = tr.y; + for (int r = 0; r < sr; r++) sy += rh[r] * yScale; + + float sw = cw[sc] * xScale; + float sh = rh[sr] * yScale; + + EditorGUI.DrawRect(new Rect(sx + 1, sy + 1, sw - 2, sh - 2), + new Color(0.18f, 0.45f, 0.85f, 0.25f)); + } + + #endregion + + #region 底部输入框 + + private static void DrawInputBar(XericUIActionTable table, float x, float y) + { + Rect barRect = new Rect(x, y, 400, 24); + 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 + }; + GUI.Label(new Rect(barRect.x + 4, barRect.y, 50, barRect.height), + $"({table.SelectedRow},{table.SelectedCol})", labelStyle); + + var inputStyle = new GUIStyle(GUI.skin.textField) + { + fontSize = 12, + margin = new RectOffset(2, 2, 2, 2) + }; + Rect inputRect = new Rect(barRect.x + 56, barRect.y + 2, 180, barRect.height - 4); + s_EditText = GUI.TextField(inputRect, s_EditText, inputStyle); + + var btnStyle = new GUIStyle(GUI.skin.button) + { + fontSize = 11, + margin = new RectOffset(2, 2, 2, 2), + padding = new RectOffset(4, 4, 2, 2) + }; + if (GUI.Button(new Rect(barRect.x + 242, barRect.y + 2, 50, barRect.height - 4), + "应用", btnStyle)) + { + ApplyEdit(table); + } + } + 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(barRect, "点击左侧行号或上方列号选中单元格", hintStyle); + } + } + + private static void ApplyEdit(XericUIActionTable table) + { + if (table.SelectedRow < 0 || table.SelectedCol < 0) return; + table.SetCellText(table.SelectedRow, table.SelectedCol, s_EditText); + table.RefreshView(); + SceneView.RepaintAll(); + } + + private static void UpdateEditText(XericUIActionTable table, int row, int col) + { + var data = table.GetCellData(row, col); + s_EditText = data != null ? data.Text ?? "" : ""; + } + + #endregion + + #region 表格区域点击 + + private static void HandleInput(XericUIActionTable table, + float[] rh, float[] cw, Rect tr, + float xScale, float yScale) + { + Event e = Event.current; + if (e.type != EventType.MouseDown || e.button != 0) return; + if (!tr.Contains(e.mousePosition)) return; + + float rx = (e.mousePosition.x - tr.x) / xScale; + float ry = (e.mousePosition.y - tr.y) / yScale; + + int col = IndexFromCoord(rx, cw); + int row = IndexFromCoord(ry, rh); + if (row < 0 || col < 0) return; + + table.SelectCell(row, col); + table.RefreshView(); + UpdateEditText(table, row, col); + + e.Use(); + SceneView.RepaintAll(); + } + + #endregion + + 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; + } + } +} diff --git a/Editor/XTable/XericUIActionTableSceneEditor.cs.meta b/Editor/XTable/XericUIActionTableSceneEditor.cs.meta new file mode 100644 index 0000000..8b2c881 --- /dev/null +++ b/Editor/XTable/XericUIActionTableSceneEditor.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: e04322ddc12583c4187e283c0752bcf6 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Core/StyleSheetUI.meta b/Runtime/Core/StyleSheetUI.meta new file mode 100644 index 0000000..3c86b72 --- /dev/null +++ b/Runtime/Core/StyleSheetUI.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: f4969d4a2ba6af0409896898d9854d05 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Core/StyleSheetUI/DefaultXericStyleSheet.xsss b/Runtime/Core/StyleSheetUI/DefaultXericStyleSheet.xsss new file mode 100644 index 0000000..2d742ac --- /dev/null +++ b/Runtime/Core/StyleSheetUI/DefaultXericStyleSheet.xsss @@ -0,0 +1,12 @@ +// ================================================== +// Xeric Style Sheet (XSSS) - 超级干燥样式表 - 默认样式 +// ================================================== + +namespace: default + +/button/TargetGraphic/ColorTint:UnityEngine.UI.ColorBlock/ { +.NormalColor = (1,1,1,1) +.HighlightedColor = (0.96,0.96,0.96,1) +.PressedColor = (0.78,0.78,0.78,1) +.DisabledColor = (0.78,0.78,0.78,0.5) +} \ No newline at end of file diff --git a/Runtime/Core/StyleSheetUI/DefaultXericStyleSheet.xsss.meta b/Runtime/Core/StyleSheetUI/DefaultXericStyleSheet.xsss.meta new file mode 100644 index 0000000..9f2630b --- /dev/null +++ b/Runtime/Core/StyleSheetUI/DefaultXericStyleSheet.xsss.meta @@ -0,0 +1,10 @@ +fileFormatVersion: 2 +guid: f5b4cf6e97fa28b429cbb9088690bfc8 +ScriptedImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 2 + userData: + assetBundleName: + assetBundleVariant: + script: {fileID: 1693830282, guid: 65aae7f37e34be6409e0966d54c89b6d, type: 3} diff --git a/Runtime/Core/StyleSheetUI/StyleSheetSelectionState.cs b/Runtime/Core/StyleSheetUI/StyleSheetSelectionState.cs new file mode 100644 index 0000000..99325e6 --- /dev/null +++ b/Runtime/Core/StyleSheetUI/StyleSheetSelectionState.cs @@ -0,0 +1,24 @@ +namespace XericUI.Core.StyleSheetUI +{ + /// + /// UI 交互状态枚举——仿 Selectable.SelectionState(protected 无法外部引用)。 + /// 使用时通过 (StyleSheetSelectionState)(int)selectableState 转换。 + /// + public enum StyleSheetSelectionState + { + /// 正常状态 + Normal, + + /// 高亮(鼠标悬停)状态 + Highlighted, + + /// 按下状态 + Pressed, + + /// 选中状态 + Selected, + + /// 禁用状态 + Disabled, + } +} diff --git a/Runtime/Core/StyleSheetUI/StyleSheetSelectionState.cs.meta b/Runtime/Core/StyleSheetUI/StyleSheetSelectionState.cs.meta new file mode 100644 index 0000000..61b6997 --- /dev/null +++ b/Runtime/Core/StyleSheetUI/StyleSheetSelectionState.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: c0ffbcf7800c40b45b11f9e9fdd88a97 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Core/StyleSheetUI/StyleSheetState.cs b/Runtime/Core/StyleSheetUI/StyleSheetState.cs new file mode 100644 index 0000000..fb75794 --- /dev/null +++ b/Runtime/Core/StyleSheetUI/StyleSheetState.cs @@ -0,0 +1,116 @@ +using System; + +using UnityEngine; + +using XericLibrary.Runtime.SuperStyleSheet; + +namespace XericUI.Core.StyleSheetUI +{ + /// + /// 样式表驱动的 UI 交互状态块——仿 ColorBlock 设计。 + /// 集中管理 Normal/Highlighted/Pressed/Selected/Disabled 五种状态的 StyleField。 + /// 职责单一:仅保存一组样式属性(如仅颜色、或仅纹理)。 + /// 在组件上声明多个实例分别实现颜色切换和纹理切换。 + /// + [Serializable] + public class StyleSheetState + { + #region 序列化字段 + + [Header("五态样式路径")] + [SerializeField] + [Tooltip("正常状态")] + private StyleField m_Normal = new StyleField(); + + [SerializeField] + [Tooltip("高亮状态")] + private StyleField m_Highlighted = new StyleField(); + + [SerializeField] + [Tooltip("按下状态")] + private StyleField m_Pressed = new StyleField(); + + [SerializeField] + [Tooltip("选中状态")] + private StyleField m_Selected = new StyleField(); + + [SerializeField] + [Tooltip("禁用状态")] + private StyleField m_Disabled = new StyleField(); + + [Header("过渡")] + [SerializeField] + [Range(0.001f, 1f)] + [Tooltip("渐变持续时间")] + private float m_FadeDuration = 0.1f; + + #endregion + + #region 公开属性 + + public StyleField Normal => m_Normal; + public StyleField Highlighted => m_Highlighted; + public StyleField Pressed => m_Pressed; + public StyleField Selected => m_Selected; + public StyleField Disabled => m_Disabled; + + public float FadeDuration + { + get => m_FadeDuration; + set => m_FadeDuration = value; + } + + /// 所有 StyleField + public StyleField[] AllFields => new[] + { + m_Normal, m_Highlighted, m_Pressed, m_Selected, m_Disabled + }; + + /// 默认实例 + public static StyleSheetState Default => new StyleSheetState(); + + #endregion + + #region 查找 + + /// 根据交互状态获取对应的 StyleField + public StyleField GetForState(StyleSheetSelectionState state) + { + switch (state) + { + case StyleSheetSelectionState.Normal: return m_Normal; + case StyleSheetSelectionState.Highlighted: return m_Highlighted; + case StyleSheetSelectionState.Pressed: return m_Pressed; + case StyleSheetSelectionState.Selected: return m_Selected; + case StyleSheetSelectionState.Disabled: return m_Disabled; + default: return m_Normal; + } + } + + #endregion + + #region 注册 / 注销 + + public void RegisterAll() + { + foreach (var f in AllFields) f?.Register(); + } + + public void UnregisterAll() + { + foreach (var f in AllFields) f?.Unregister(); + } + + public void SubscribeAll(Action callback) + { + foreach (var f in AllFields) { if (f != null) f.OnValueChanged += callback; } + } + + public void UnsubscribeAll(Action callback) + { + foreach (var f in AllFields) { if (f != null) f.OnValueChanged -= callback; } + } + + #endregion + } +} diff --git a/Runtime/Core/StyleSheetUI/StyleSheetState.cs.meta b/Runtime/Core/StyleSheetUI/StyleSheetState.cs.meta new file mode 100644 index 0000000..f6991c0 --- /dev/null +++ b/Runtime/Core/StyleSheetUI/StyleSheetState.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 3f8a47a476054d142a8987d093216e74 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Lrss3.Xericuiactionvessel.asmdef b/Runtime/Lrss3.Xericuiactionvessel.asmdef index 6ce51c6..6cea7b4 100644 --- a/Runtime/Lrss3.Xericuiactionvessel.asmdef +++ b/Runtime/Lrss3.Xericuiactionvessel.asmdef @@ -4,7 +4,9 @@ "references": [ "UnityEngine.UI", "Unity.TextMeshPro", - "Lrss3.Deconstruction" + "Lrss3.Deconstruction", + "Unity.Burst", + "Unity.Collections" ], "includePlatforms": [], "excludePlatforms": [], diff --git a/Runtime/OverrideUI/XericButton.cs b/Runtime/OverrideUI/XericButton.cs index bf5de56..b5602be 100644 --- a/Runtime/OverrideUI/XericButton.cs +++ b/Runtime/OverrideUI/XericButton.cs @@ -1,4 +1,4 @@ -using TMPro; +using TMPro; using UnityEngine; using UnityEngine.UI; using XericUI.Helper; @@ -168,11 +168,11 @@ namespace XericUI.OverrideUI #region 组件替换 #if UNITY_EDITOR - [UnityEditor.MenuItem("CONTEXT/Button/Replace To XericButton", true)] + [UnityEditor.MenuItem("CONTEXT/Button/Replace To/Xeric Button", true)] private static bool ValidateReplaceToXeric(UnityEditor.MenuCommand command) => command.context is Button and not XericButton; - [UnityEditor.MenuItem("CONTEXT/Button/Replace To XericButton", false, 10)] + [UnityEditor.MenuItem("CONTEXT/Button/Replace To/Xeric Button", false, 10)] private static void ReplaceToXeric(UnityEditor.MenuCommand command) => command.ReplaceCommandContext( @@ -184,11 +184,11 @@ namespace XericUI.OverrideUI t.onClick = d.onClick; }); - [UnityEditor.MenuItem("CONTEXT/Button/Replace To Button", true)] + [UnityEditor.MenuItem("CONTEXT/Button/Replace To/Unity Button", true)] private static bool ValidateReplaceToUnity(UnityEditor.MenuCommand command) => command.context is XericButton; - [UnityEditor.MenuItem("CONTEXT/Button/Replace To Button", false, 10)] + [UnityEditor.MenuItem("CONTEXT/Button/Replace To/Unity Button", false, 100)] private static void ReplaceToUnity(UnityEditor.MenuCommand command) => command.ReplaceCommandContext( diff --git a/Runtime/OverrideUI/XericImage.cs b/Runtime/OverrideUI/XericImage.cs index b10ab27..ff758d0 100644 --- a/Runtime/OverrideUI/XericImage.cs +++ b/Runtime/OverrideUI/XericImage.cs @@ -1,4 +1,4 @@ -using System; +using System; using UnityEngine; using UnityEngine.Serialization; using UnityEngine.UI; @@ -27,11 +27,11 @@ namespace XericUI.OverrideUI #region 组件替换 #if UNITY_EDITOR - [UnityEditor.MenuItem("CONTEXT/Image/Replace To XericImage", true)] + [UnityEditor.MenuItem("CONTEXT/Image/Replace To/Xeric Image", true)] private static bool ValidateReplaceToXeric(UnityEditor.MenuCommand command) => command.context is Image and not XericImage; - [UnityEditor.MenuItem("CONTEXT/Image/Replace To XericImage", false, 10)] + [UnityEditor.MenuItem("CONTEXT/Image/Replace To/Xeric Image", false, 10)] private static void ReplaceToXeric(UnityEditor.MenuCommand command) => command.ReplaceCommandContext( o => @@ -41,11 +41,11 @@ namespace XericUI.OverrideUI t.sprite = d; }); - [UnityEditor.MenuItem("CONTEXT/Image/Replace To Image", true)] + [UnityEditor.MenuItem("CONTEXT/Image/Replace To/Unity Image", true)] private static bool ValidateReplaceToUnity(UnityEditor.MenuCommand command) => command.context is XericImage; - [UnityEditor.MenuItem("CONTEXT/Image/Replace To Image", false, 10)] + [UnityEditor.MenuItem("CONTEXT/Image/Replace To/Unity Image", false, 100)] private static void ReplaceToUnity(UnityEditor.MenuCommand command) => command.ReplaceCommandContext( o => diff --git a/Runtime/OverrideUI/XericStyleButton.cs b/Runtime/OverrideUI/XericStyleButton.cs new file mode 100644 index 0000000..45e7ff5 --- /dev/null +++ b/Runtime/OverrideUI/XericStyleButton.cs @@ -0,0 +1,172 @@ +using UnityEngine; +using UnityEngine.EventSystems; +using UnityEngine.UI; + +using XericLibrary.Runtime.SuperStyleSheet; +using XericUI.Core.StyleSheetUI; +using XericUI.Helper; + +namespace XericUI.OverrideUI +{ + /// + /// 基于 SuperStyleSheet 的按钮组件。 + /// 通过两个独立的 StyleSheetState 实例分别管理颜色和纹理的五态样式切换, + /// 替代 Unity 内置的 ColorBlock 和 SpriteState。 + /// + [AddComponentMenu("Xeric UI Vessel/UI/Style Button", 51)] + public class XericStyleButton : Button + { + #region 序列化字段 + + [Header("样式表状态")] + [SerializeField] + [Tooltip("五态颜色样式块")] + private StyleSheetState m_ColorState = StyleSheetState.Default; + + [SerializeField] + [Tooltip("五态纹理样式块")] + private StyleSheetState m_TextureState = StyleSheetState.Default; + + [SerializeField] + [Tooltip("启用样式表驱动过渡(关闭则回退为 Unity ColorBlock)")] + private bool m_EnableStyleTransition = true; + + #endregion + + #region 运行时 + + [System.NonSerialized] private StyleSheetSelectionState m_CurrentState = StyleSheetSelectionState.Normal; + [System.NonSerialized] private Graphic m_TargetGraphic; + [System.NonSerialized] private Image m_TargetImage; + + #endregion + + #region 生命周期 + + protected override void Awake() + { + base.Awake(); + m_TargetGraphic = targetGraphic; + m_TargetImage = m_TargetGraphic as Image; + } + + protected override void OnEnable() + { + base.OnEnable(); + + m_ColorState?.RegisterAll(); + m_TextureState?.RegisterAll(); + m_ColorState?.SubscribeAll(OnStyleChanged); + m_TextureState?.SubscribeAll(OnStyleChanged); + + ApplyForState(m_CurrentState); + } + + protected override void OnDisable() + { + m_ColorState?.UnsubscribeAll(OnStyleChanged); + m_TextureState?.UnsubscribeAll(OnStyleChanged); + m_ColorState?.UnregisterAll(); + m_TextureState?.UnregisterAll(); + + base.OnDisable(); + } + + #endregion + + #region 状态过渡 + + protected override void DoStateTransition(SelectionState state, bool instant) + { + if (!gameObject.activeInHierarchy) return; + + m_CurrentState = (StyleSheetSelectionState)(int)state; + + if (m_EnableStyleTransition) + ApplyForState(m_CurrentState, instant); + else + base.DoStateTransition(state, instant); + } + + #endregion + + #region 样式应用 + + private void ApplyForState(StyleSheetSelectionState state, bool instant = false) + { + // 颜色 + if (m_ColorState != null) + { + StyleField colorField = m_ColorState.GetForState(state); + if (colorField != null && m_TargetGraphic != null) + { + StyleValue val = colorField.GetValue(); + if (val != null) + m_TargetGraphic.CrossFadeColor((Color)val, + instant ? 0f : m_ColorState.FadeDuration, true, true); + } + } + + // 纹理 + if (m_TextureState != null && m_TargetImage != null) + { + StyleField texField = m_TextureState.GetForState(state); + if (texField != null) + { + StyleValue val = texField.GetValue(); + Texture2D tex = val?.GetValueAs(); + if (tex != null) + m_TargetImage.overrideSprite = Sprite.Create(tex, + new Rect(0, 0, tex.width, tex.height), + new Vector2(0.5f, 0.5f)); + } + } + } + + private void OnStyleChanged(StyleValue _) + { + if (gameObject.activeInHierarchy) + ApplyForState(m_CurrentState); + } + + #endregion + + #region 组件替换 + +#if UNITY_EDITOR + [UnityEditor.MenuItem("CONTEXT/Button/Replace To/Xeric (Style)", true)] + private static bool ValidateReplaceToStyleButton(UnityEditor.MenuCommand command) + => command.context is Button and not XericStyleButton; + + [UnityEditor.MenuItem("CONTEXT/Button/Replace To/Xeric (Style)", false, 11)] + private static void ReplaceToStyleButton(UnityEditor.MenuCommand command) + => command.ReplaceCommandContext( + o => (o.targetGraphic, o.onClick, o.navigation), + (t, d) => + { + t.targetGraphic = d.targetGraphic; + t.onClick = d.onClick; + t.navigation = d.navigation; + }); + + [UnityEditor.MenuItem("CONTEXT/Button/Replace To/Unity Button", true)] + private static bool ValidateReplaceStyleToUnity(UnityEditor.MenuCommand command) + => command.context is XericStyleButton; + + [UnityEditor.MenuItem("CONTEXT/Button/Replace To/Unity Button", false, 100)] + private static void ReplaceStyleToUnity(UnityEditor.MenuCommand command) + => command.ReplaceCommandContext( + o => (o.targetGraphic, o.onClick, o.navigation), + (t, d) => + { + t.targetGraphic = d.targetGraphic; + t.onClick = d.onClick; + t.navigation = d.navigation; + }); +#endif + + #endregion + } +} diff --git a/Runtime/OverrideUI/XericStyleButton.cs.meta b/Runtime/OverrideUI/XericStyleButton.cs.meta new file mode 100644 index 0000000..dab39a3 --- /dev/null +++ b/Runtime/OverrideUI/XericStyleButton.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: ac135f8a0c9ac114aabb20e033ab1331 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/OverrideUI/XericStyleImage.cs b/Runtime/OverrideUI/XericStyleImage.cs new file mode 100644 index 0000000..e175f14 --- /dev/null +++ b/Runtime/OverrideUI/XericStyleImage.cs @@ -0,0 +1,140 @@ +using UnityEngine; +using UnityEngine.UI; + +using XericLibrary.Runtime.SuperStyleSheet; +using XericUI.Helper; + +namespace XericUI.OverrideUI +{ + /// + /// 基于 SuperStyleSheet 的图像组件。 + /// 使用 StyleField 引用样式表中的颜色和纹理路径,样式变更时自动刷新。 + /// + [AddComponentMenu("Xeric UI Vessel/UI/Style Image", 53)] + public class XericStyleImage : Image + { + #region 序列化字段 + + [Header("图像样式")] + [SerializeField] + [Tooltip("颜色样式路径")] + private StyleField m_ColorStyle = new StyleField(); + + [SerializeField] + [Tooltip("纹理/精灵样式路径")] + private StyleField m_TextureStyle = new StyleField(); + + [SerializeField] + [Tooltip("材质样式路径")] + private StyleField m_MaterialStyle = new StyleField(); + + [SerializeField] + [Tooltip("启用样式表自动刷新")] + private bool m_AutoRefresh = true; + + #endregion + + #region 运行时 + + [System.NonSerialized] private Sprite m_ResolvedSprite; + + #endregion + + #region 生命周期 + + protected override void OnEnable() + { + base.OnEnable(); + + if (!m_AutoRefresh) return; + + m_ColorStyle?.Register(); + m_TextureStyle?.Register(); + m_MaterialStyle?.Register(); + m_ColorStyle.OnValueChanged += OnStyleChanged; + m_TextureStyle.OnValueChanged += OnStyleChanged; + m_MaterialStyle.OnValueChanged += OnStyleChanged; + + ApplyAllStyles(); + } + + protected override void OnDisable() + { + if (m_ColorStyle != null) { m_ColorStyle.OnValueChanged -= OnStyleChanged; m_ColorStyle.Unregister(); } + if (m_TextureStyle != null) { m_TextureStyle.OnValueChanged -= OnStyleChanged; m_TextureStyle.Unregister(); } + if (m_MaterialStyle != null) { m_MaterialStyle.OnValueChanged -= OnStyleChanged; m_MaterialStyle.Unregister(); } + + base.OnDisable(); + } + + #endregion + + #region 样式应用 + + public void ApplyAllStyles() + { + if (m_ColorStyle != null) + { + StyleValue val = m_ColorStyle.GetValue(); + if (val != null) color = val; + } + + if (m_TextureStyle != null) + { + StyleValue val = m_TextureStyle.GetValue(); + Sprite sp = val?.GetValueAs(); + if (sp != null) { m_ResolvedSprite = sp; sprite = sp; } + else + { + Texture2D tex = val?.GetValueAs(); + if (tex != null) + { + m_ResolvedSprite = Sprite.Create(tex, new Rect(0, 0, tex.width, tex.height), new Vector2(0.5f, 0.5f)); + sprite = m_ResolvedSprite; + } + } + } + + if (m_MaterialStyle != null) + { + StyleValue val = m_MaterialStyle.GetValue(); + Material mat = val?.GetValueAs(); + if (mat != null) material = mat; + } + } + + private void OnStyleChanged(StyleValue val) + { + if (gameObject.activeInHierarchy) + ApplyAllStyles(); + } + + #endregion + + #region 组件替换 + +#if UNITY_EDITOR + [UnityEditor.MenuItem("CONTEXT/Image/Replace To/Xeric (Style)", true)] + private static bool ValidateReplaceToStyleImage(UnityEditor.MenuCommand command) + => command.context is Image and not XericStyleImage; + + [UnityEditor.MenuItem("CONTEXT/Image/Replace To/Xeric (Style)", false, 11)] + private static void ReplaceToStyleImage(UnityEditor.MenuCommand command) + => command.ReplaceCommandContext( + o => o.sprite, + (t, d) => { t.sprite = d; }); + + [UnityEditor.MenuItem("CONTEXT/Image/Replace To/Unity Image", true)] + private static bool ValidateReplaceStyleImageToUnity(UnityEditor.MenuCommand command) + => command.context is XericStyleImage; + + [UnityEditor.MenuItem("CONTEXT/Image/Replace To/Unity Image", false, 100)] + private static void ReplaceStyleImageToUnity(UnityEditor.MenuCommand command) + => command.ReplaceCommandContext( + o => o.sprite, + (t, d) => { t.sprite = d; }); +#endif + + #endregion + } +} diff --git a/Runtime/OverrideUI/XericStyleImage.cs.meta b/Runtime/OverrideUI/XericStyleImage.cs.meta new file mode 100644 index 0000000..4b9f71b --- /dev/null +++ b/Runtime/OverrideUI/XericStyleImage.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: dff7107391ce75149acdb57d775d7d3c +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/OverrideUI/XericStyleToggle.cs b/Runtime/OverrideUI/XericStyleToggle.cs new file mode 100644 index 0000000..6c0b83a --- /dev/null +++ b/Runtime/OverrideUI/XericStyleToggle.cs @@ -0,0 +1,209 @@ +using UnityEngine; +using UnityEngine.UI; + +using XericLibrary.Runtime.SuperStyleSheet; +using XericUI.Core.StyleSheetUI; +using XericUI.Helper; + +namespace XericUI.OverrideUI +{ + /// + /// 基于 SuperStyleSheet 的开关组件。 + /// 使用两个 StyleSheetState 实例管理五态颜色和纹理样式, + /// 额外支持 isOn 覆盖和 Checkmark 样式。 + /// + [AddComponentMenu("Xeric UI Vessel/UI/Style Toggle", 52)] + public class XericStyleToggle : Toggle + { + #region 序列化字段 + + [Header("样式表状态")] + [SerializeField] + private StyleSheetState m_ColorState = StyleSheetState.Default; + + [SerializeField] + private StyleSheetState m_TextureState = StyleSheetState.Default; + + [Header("Toggle 专用")] + [SerializeField] + [Tooltip("isOn=true 时的颜色覆盖")] + private StyleField m_OnOverrideColor = new StyleField(); + + [SerializeField] + [Tooltip("isOn=true 时的纹理覆盖")] + private StyleField m_OnOverrideTexture = new StyleField(); + + [SerializeField] + [Tooltip("Checkmark 打勾标志的样式")] + private StyleField m_CheckmarkStyle = new StyleField(); + + [SerializeField] + [Tooltip("启用样式表驱动过渡")] + private bool m_EnableStyleTransition = true; + + #endregion + + #region 运行时 + + [System.NonSerialized] private StyleSheetSelectionState m_CurrentState = StyleSheetSelectionState.Normal; + [System.NonSerialized] private Graphic m_TargetGraphic; + [System.NonSerialized] private Image m_TargetImage; + [System.NonSerialized] private Graphic m_CheckmarkGraphic; + [System.NonSerialized] private Image m_CheckmarkImage; + + #endregion + + #region 生命周期 + + protected override void Awake() + { + base.Awake(); + m_TargetGraphic = targetGraphic; + m_TargetImage = m_TargetGraphic as Image; + + Transform checkmark = transform.Find("Background/Checkmark") + ?? transform.Find("Checkmark"); + if (checkmark != null) + { + m_CheckmarkGraphic = checkmark.GetComponent(); + m_CheckmarkImage = checkmark.GetComponent(); + } + } + + protected override void OnEnable() + { + base.OnEnable(); + + m_ColorState?.RegisterAll(); + m_TextureState?.RegisterAll(); + m_ColorState?.SubscribeAll(OnStyleChanged); + m_TextureState?.SubscribeAll(OnStyleChanged); + + m_OnOverrideColor?.Register(); + m_OnOverrideColor.OnValueChanged += OnStyleChanged; + m_OnOverrideTexture?.Register(); + m_OnOverrideTexture.OnValueChanged += OnStyleChanged; + m_CheckmarkStyle?.Register(); + m_CheckmarkStyle.OnValueChanged += OnStyleChanged; + + onValueChanged.AddListener(OnToggleValueChanged); + ApplyForState(m_CurrentState); + } + + protected override void OnDisable() + { + onValueChanged.RemoveListener(OnToggleValueChanged); + + m_ColorState?.UnsubscribeAll(OnStyleChanged); + m_TextureState?.UnsubscribeAll(OnStyleChanged); + m_ColorState?.UnregisterAll(); + m_TextureState?.UnregisterAll(); + if (m_OnOverrideColor != null) { m_OnOverrideColor.OnValueChanged -= OnStyleChanged; m_OnOverrideColor.Unregister(); } + if (m_OnOverrideTexture != null) { m_OnOverrideTexture.OnValueChanged -= OnStyleChanged; m_OnOverrideTexture.Unregister(); } + if (m_CheckmarkStyle != null) { m_CheckmarkStyle.OnValueChanged -= OnStyleChanged; m_CheckmarkStyle.Unregister(); } + + base.OnDisable(); + } + + #endregion + + #region 状态过渡 + + protected override void DoStateTransition(SelectionState state, bool instant) + { + if (!gameObject.activeInHierarchy) return; + m_CurrentState = (StyleSheetSelectionState)(int)state; + + if (m_EnableStyleTransition) + ApplyForState(m_CurrentState, instant); + else + base.DoStateTransition(state, instant); + } + + private void OnToggleValueChanged(bool _) + { + if (m_EnableStyleTransition) + ApplyForState(m_CurrentState); + } + + #endregion + + #region 样式应用 + + private void ApplyForState(StyleSheetSelectionState state, bool instant = false) + { + // 颜色:On 覆盖优先 + StyleField colorField = isOn && m_OnOverrideColor?.GetValue() != null + ? m_OnOverrideColor + : m_ColorState?.GetForState(state); + + if (colorField != null && m_TargetGraphic != null) + { + StyleValue val = colorField.GetValue(); + if (val != null) + { + float dur = instant ? 0f : (m_ColorState?.FadeDuration ?? 0.1f); + m_TargetGraphic.CrossFadeColor((Color)val, dur, true, true); + } + } + + // 纹理:On 覆盖优先 + StyleField texField = isOn && m_OnOverrideTexture?.GetValue() != null + ? m_OnOverrideTexture + : m_TextureState?.GetForState(state); + + if (texField != null && m_TargetImage != null) + { + StyleValue val = texField.GetValue(); + Texture2D tex = val?.GetValueAs(); + if (tex != null) + m_TargetImage.overrideSprite = Sprite.Create(tex, + new Rect(0, 0, tex.width, tex.height), + new Vector2(0.5f, 0.5f)); + } + + // Checkmark + if (m_CheckmarkGraphic != null && m_CheckmarkStyle != null) + { + StyleValue val = m_CheckmarkStyle.GetValue(); + if (val != null) m_CheckmarkGraphic.color = val; + } + } + + private void OnStyleChanged(StyleValue _) + { + if (gameObject.activeInHierarchy) + ApplyForState(m_CurrentState); + } + + #endregion + + #region 组件替换 + +#if UNITY_EDITOR + [UnityEditor.MenuItem("CONTEXT/Toggle/Replace To/Xeric (Style)", true)] + private static bool ValidateReplaceToStyleToggle(UnityEditor.MenuCommand command) + => command.context is Toggle and not XericStyleToggle; + + [UnityEditor.MenuItem("CONTEXT/Toggle/Replace To/Xeric (Style)", false, 11)] + private static void ReplaceToStyleToggle(UnityEditor.MenuCommand command) + => command.ReplaceCommandContext( + o => (o.graphic, o.group, o.onValueChanged, o.navigation), + (t, d) => { t.graphic = d.graphic; t.group = d.group; t.onValueChanged = d.onValueChanged; t.navigation = d.navigation; }); + + [UnityEditor.MenuItem("CONTEXT/Toggle/Replace To/Unity Toggle", true)] + private static bool ValidateReplaceStyleToggleToUnity(UnityEditor.MenuCommand command) + => command.context is XericStyleToggle; + + [UnityEditor.MenuItem("CONTEXT/Toggle/Replace To/Unity Toggle", false, 100)] + private static void ReplaceStyleToggleToUnity(UnityEditor.MenuCommand command) + => command.ReplaceCommandContext( + o => (o.graphic, o.group, o.onValueChanged, o.navigation), + (t, d) => { t.graphic = d.graphic; t.group = d.group; t.onValueChanged = d.onValueChanged; t.navigation = d.navigation; }); +#endif + + #endregion + } +} diff --git a/Runtime/OverrideUI/XericStyleToggle.cs.meta b/Runtime/OverrideUI/XericStyleToggle.cs.meta new file mode 100644 index 0000000..d7d49e7 --- /dev/null +++ b/Runtime/OverrideUI/XericStyleToggle.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: f4002f96b2337614f96d13d1beeb2542 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/OverrideUI/XericToggle.cs b/Runtime/OverrideUI/XericToggle.cs index 0635308..20bb1c2 100644 --- a/Runtime/OverrideUI/XericToggle.cs +++ b/Runtime/OverrideUI/XericToggle.cs @@ -89,7 +89,7 @@ namespace XericUI m_ximage = graphic.GetComponent(); if (m_ximage) return m_ximage; - Debug.Log("עÒâÌ滻toggleµÄcheckmarkΪXericImageÒÔ֧³ÖÍêÕûµĹ¦ÄÜ"); + Debug.Log("ע���滻toggle��checkmarkΪXericImage��֧�������Ĺ���"); return null; } } @@ -226,13 +226,13 @@ namespace XericUI #endif } - #region ×é¼þÌ滻 + #region ����滻 #if UNITY_EDITOR - [UnityEditor.MenuItem("CONTEXT/Toggle/Replace To XericToggle", true)] + [UnityEditor.MenuItem("CONTEXT/Toggle/Replace To/Xeric Toggle", true)] private static bool ValidateReplaceToXeric(UnityEditor.MenuCommand command) => command.context is Toggle and not XericToggle; - [UnityEditor.MenuItem("CONTEXT/Toggle/Replace To XericToggle", false, 10)] + [UnityEditor.MenuItem("CONTEXT/Toggle/Replace To/Xeric Toggle", false, 10)] private static void ReplaceToXeric(UnityEditor.MenuCommand command) { command.ReplaceCommandContext command.context is XericToggle; - [UnityEditor.MenuItem("CONTEXT/Toggle/Replace To Toggle", false, 10)] + [UnityEditor.MenuItem("CONTEXT/Toggle/Replace To/Unity Toggle", false, 100)] private static void ReplaceToUnity(UnityEditor.MenuCommand command) => command.ReplaceCommandContext( diff --git a/Runtime/OverrideUI/XericToggleGroup.cs b/Runtime/OverrideUI/XericToggleGroup.cs index 393b208..04dbe30 100644 --- a/Runtime/OverrideUI/XericToggleGroup.cs +++ b/Runtime/OverrideUI/XericToggleGroup.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using UnityEngine; using UnityEngine.UI; using XericUI.Helper; @@ -12,19 +12,19 @@ namespace XericUI.OverrideUI #region 组件替换 #if UNITY_EDITOR - [UnityEditor.MenuItem("CONTEXT/ToggleGroup/Replace To XericToggleGroup", true)] + [UnityEditor.MenuItem("CONTEXT/ToggleGroup/Replace To/Xeric ToggleGroup", true)] private static bool ValidateReplaceToXeric(UnityEditor.MenuCommand command) => command.context is ToggleGroup and not XericToggleGroup; - [UnityEditor.MenuItem("CONTEXT/ToggleGroup/Replace To XericToggleGroup", false, 10)] + [UnityEditor.MenuItem("CONTEXT/ToggleGroup/Replace To/Xeric ToggleGroup", false, 10)] private static void ReplaceToXeric(UnityEditor.MenuCommand command) => command.ReplaceCommandContext(); - [UnityEditor.MenuItem("CONTEXT/ToggleGroup/Replace To ToggleGroup", true)] + [UnityEditor.MenuItem("CONTEXT/ToggleGroup/Replace To/Unity ToggleGroup", true)] private static bool ValidateReplaceToUnity(UnityEditor.MenuCommand command) => command.context is XericToggleGroup; - [UnityEditor.MenuItem("CONTEXT/ToggleGroup/Replace To ToggleGroup", false, 10)] + [UnityEditor.MenuItem("CONTEXT/ToggleGroup/Replace To/Unity ToggleGroup", false, 100)] private static void ReplaceToUnity(UnityEditor.MenuCommand command) => command.ReplaceCommandContext(); #endif diff --git a/Runtime/OverrideUI/XericUIrAttributeButton.cs b/Runtime/OverrideUI/XericUIrAttributeButton.cs index f9f034b..4e16bf6 100644 --- a/Runtime/OverrideUI/XericUIrAttributeButton.cs +++ b/Runtime/OverrideUI/XericUIrAttributeButton.cs @@ -27,22 +27,22 @@ namespace XericUI.OverrideUI #region 组件替换 #if UNITY_EDITOR - [UnityEditor.MenuItem("CONTEXT/Button/Replace To XericUIrAttributeButton", true)] - private static bool ValidateReplaceToAttribute(UnityEditor.MenuCommand command) + [UnityEditor.MenuItem("CONTEXT/Button/Replace To/Xeric (Attribute)", true)] + private static bool ValidateReplaceToXeric(UnityEditor.MenuCommand command) => command.context is Button and not XericUIrAttributeButton; - [UnityEditor.MenuItem("CONTEXT/Button/Replace To XericUIrAttributeButton", false, 11)] - private static void ReplaceToAttribute(UnityEditor.MenuCommand command) + [UnityEditor.MenuItem("CONTEXT/Button/Replace To/Xeric (Attribute)", false, 12)] + private static void ReplaceToXeric(UnityEditor.MenuCommand command) => command.ReplaceCommandContext( o => (o.targetGraphic, o.onClick), (t, d) => { t.targetGraphic = d.targetGraphic; t.onClick = d.onClick; }); - [UnityEditor.MenuItem("CONTEXT/Button/Replace To Button", true)] + [UnityEditor.MenuItem("CONTEXT/Button/Replace To/Unity Button", true)] private static bool ValidateReplaceToUnity(UnityEditor.MenuCommand command) => command.context is XericUIrAttributeButton; - [UnityEditor.MenuItem("CONTEXT/Button/Replace To Button", false, 11)] + [UnityEditor.MenuItem("CONTEXT/Button/Replace To/Unity Button", false, 100)] private static void ReplaceToUnity(UnityEditor.MenuCommand command) => command.ReplaceCommandContext( diff --git a/Runtime/OverrideUI/XericUIrAttributeDropdown.cs b/Runtime/OverrideUI/XericUIrAttributeDropdown.cs index 62590de..07bea43 100644 --- a/Runtime/OverrideUI/XericUIrAttributeDropdown.cs +++ b/Runtime/OverrideUI/XericUIrAttributeDropdown.cs @@ -27,11 +27,11 @@ namespace XericUI.OverrideUI #region 组件替换 #if UNITY_EDITOR - [UnityEditor.MenuItem("CONTEXT/Dropdown/Replace To XericUIrAttributeDropdown", true)] + [UnityEditor.MenuItem("CONTEXT/Dropdown/Replace To/Xeric (Attribute)", true)] private static bool ValidateReplaceToAttribute(UnityEditor.MenuCommand command) => command.context is Dropdown and not XericUIrAttributeDropdown; - [UnityEditor.MenuItem("CONTEXT/Dropdown/Replace To XericUIrAttributeDropdown", false, 10)] + [UnityEditor.MenuItem("CONTEXT/Dropdown/Replace To/Xeric (Attribute)", false, 10)] private static void ReplaceToAttribute(UnityEditor.MenuCommand command) => command.ReplaceCommandContext options, DropdownEvent onValueChanged)>( @@ -45,11 +45,11 @@ namespace XericUI.OverrideUI t.onValueChanged = d.onValueChanged; }); - [UnityEditor.MenuItem("CONTEXT/Dropdown/Replace To Dropdown", true)] + [UnityEditor.MenuItem("CONTEXT/Dropdown/Replace To/Unity Dropdown", true)] private static bool ValidateReplaceToUnity(UnityEditor.MenuCommand command) => command.context is XericUIrAttributeDropdown; - [UnityEditor.MenuItem("CONTEXT/Dropdown/Replace To Dropdown", false, 10)] + [UnityEditor.MenuItem("CONTEXT/Dropdown/Replace To/Unity Dropdown", false, 100)] private static void ReplaceToUnity(UnityEditor.MenuCommand command) => command.ReplaceCommandContext options, DropdownEvent onValueChanged)>( diff --git a/Runtime/OverrideUI/XericUIrAttributeInputField.cs b/Runtime/OverrideUI/XericUIrAttributeInputField.cs index 981b881..96a6e0d 100644 --- a/Runtime/OverrideUI/XericUIrAttributeInputField.cs +++ b/Runtime/OverrideUI/XericUIrAttributeInputField.cs @@ -26,11 +26,11 @@ namespace XericUI.OverrideUI #region 组件替换 #if UNITY_EDITOR - [UnityEditor.MenuItem("CONTEXT/InputField/Replace To XericUIrAttributeInputField", true)] + [UnityEditor.MenuItem("CONTEXT/InputField/Replace To/Xeric (Attribute)", true)] private static bool ValidateReplaceToAttribute(UnityEditor.MenuCommand command) => command.context is InputField and not XericUIrAttributeInputField; - [UnityEditor.MenuItem("CONTEXT/InputField/Replace To XericUIrAttributeInputField", false, 10)] + [UnityEditor.MenuItem("CONTEXT/InputField/Replace To/Xeric (Attribute)", false, 10)] private static void ReplaceToAttribute(UnityEditor.MenuCommand command) => command.ReplaceCommandContext( @@ -43,11 +43,11 @@ namespace XericUI.OverrideUI t.onEndEdit = d.onEndEdit; }); - [UnityEditor.MenuItem("CONTEXT/InputField/Replace To InputField", true)] + [UnityEditor.MenuItem("CONTEXT/InputField/Replace To/Unity InputField", true)] private static bool ValidateReplaceToUnity(UnityEditor.MenuCommand command) => command.context is XericUIrAttributeInputField; - [UnityEditor.MenuItem("CONTEXT/InputField/Replace To InputField", false, 10)] + [UnityEditor.MenuItem("CONTEXT/InputField/Replace To/Unity InputField", false, 100)] private static void ReplaceToUnity(UnityEditor.MenuCommand command) => command.ReplaceCommandContext( diff --git a/Runtime/OverrideUI/XericUIrAttributeSlider.cs b/Runtime/OverrideUI/XericUIrAttributeSlider.cs index 675c005..47d4d83 100644 --- a/Runtime/OverrideUI/XericUIrAttributeSlider.cs +++ b/Runtime/OverrideUI/XericUIrAttributeSlider.cs @@ -26,11 +26,11 @@ namespace XericUI.OverrideUI #region 组件替换 #if UNITY_EDITOR - [UnityEditor.MenuItem("CONTEXT/Slider/Replace To XericUIrAttributeSlider", true)] + [UnityEditor.MenuItem("CONTEXT/Slider/Replace To/Xeric (Attribute)", true)] private static bool ValidateReplaceToAttribute(UnityEditor.MenuCommand command) => command.context is Slider and not XericUIrAttributeSlider; - [UnityEditor.MenuItem("CONTEXT/Slider/Replace To XericUIrAttributeSlider", false, 10)] + [UnityEditor.MenuItem("CONTEXT/Slider/Replace To/Xeric (Attribute)", false, 10)] private static void ReplaceToAttribute(UnityEditor.MenuCommand command) => command.ReplaceCommandContext( @@ -43,11 +43,11 @@ namespace XericUI.OverrideUI t.onValueChanged = d.onValueChanged; }); - [UnityEditor.MenuItem("CONTEXT/Slider/Replace To Slider", true)] + [UnityEditor.MenuItem("CONTEXT/Slider/Replace To/Unity Slider", true)] private static bool ValidateReplaceToUnity(UnityEditor.MenuCommand command) => command.context is XericUIrAttributeSlider; - [UnityEditor.MenuItem("CONTEXT/Slider/Replace To Slider", false, 10)] + [UnityEditor.MenuItem("CONTEXT/Slider/Replace To/Unity Slider", false, 100)] private static void ReplaceToUnity(UnityEditor.MenuCommand command) => command.ReplaceCommandContext( diff --git a/Runtime/OverrideUI/XericUIrAttributeText.cs b/Runtime/OverrideUI/XericUIrAttributeText.cs index 2f5fde9..5826843 100644 --- a/Runtime/OverrideUI/XericUIrAttributeText.cs +++ b/Runtime/OverrideUI/XericUIrAttributeText.cs @@ -45,21 +45,21 @@ namespace XericUI.OverrideUI #region 组件替换 #if UNITY_EDITOR - [UnityEditor.MenuItem("CONTEXT/Image/Replace To XericUIrAttributeText", true)] + [UnityEditor.MenuItem("CONTEXT/Image/Replace To/Xeric (AttributeText)", true)] private static bool ValidateReplaceToAttribute(UnityEditor.MenuCommand command) => command.context is Image and not XericUIrAttributeText; - [UnityEditor.MenuItem("CONTEXT/Image/Replace To XericUIrAttributeText", false, 11)] + [UnityEditor.MenuItem("CONTEXT/Image/Replace To/Xeric (AttributeText)", false, 13)] private static void ReplaceToAttribute(UnityEditor.MenuCommand command) => command.ReplaceCommandContext( o => o.sprite, (t, d) => { t.sprite = d; }); - [UnityEditor.MenuItem("CONTEXT/Image/Replace To Image", true)] + [UnityEditor.MenuItem("CONTEXT/Image/Replace To/Unity Image", true)] private static bool ValidateReplaceToUnity(UnityEditor.MenuCommand command) => command.context is XericUIrAttributeText; - [UnityEditor.MenuItem("CONTEXT/Image/Replace To Image", false, 11)] + [UnityEditor.MenuItem("CONTEXT/Image/Replace To/Unity Image", false, 100)] private static void ReplaceToUnity(UnityEditor.MenuCommand command) => command.ReplaceCommandContext( o => o.sprite, diff --git a/Runtime/OverrideUI/XericUIrAttributeTextMeshProInputField.cs b/Runtime/OverrideUI/XericUIrAttributeTextMeshProInputField.cs index cb8ad2a..f125b5f 100644 --- a/Runtime/OverrideUI/XericUIrAttributeTextMeshProInputField.cs +++ b/Runtime/OverrideUI/XericUIrAttributeTextMeshProInputField.cs @@ -59,7 +59,7 @@ namespace XericUI.OverrideUI #region 组件替换 #if UNITY_EDITOR - [UnityEditor.MenuItem("CONTEXT/TMP_InputField/Replace To XericUIrAttributeTextMeshProInputField", true)] + [UnityEditor.MenuItem("CONTEXT/TMP_InputField/Replace To/Xeric (Attribute)", true)] private static bool ValidateReplaceToAttribute(UnityEditor.MenuCommand command) { #if dUI_TextMeshPro @@ -69,7 +69,7 @@ namespace XericUI.OverrideUI #endif } - [UnityEditor.MenuItem("CONTEXT/TMP_InputField/Replace To XericUIrAttributeTextMeshProInputField", false, 10)] + [UnityEditor.MenuItem("CONTEXT/TMP_InputField/Replace To/Xeric (Attribute)", false, 10)] private static void ReplaceToAttribute(UnityEditor.MenuCommand command) { #if dUI_TextMeshPro @@ -79,11 +79,11 @@ namespace XericUI.OverrideUI #endif } - [UnityEditor.MenuItem("CONTEXT/TMP_InputField/Replace To TMP_InputField", true)] + [UnityEditor.MenuItem("CONTEXT/TMP_InputField/Replace To/Unity TMP_InputField", true)] private static bool ValidateReplaceToUnity(UnityEditor.MenuCommand command) => command.context is XericUIrAttributeTextMeshProInputField; - [UnityEditor.MenuItem("CONTEXT/TMP_InputField/Replace To TMP_InputField", false, 10)] + [UnityEditor.MenuItem("CONTEXT/TMP_InputField/Replace To/Unity TMP_InputField", false, 100)] private static void ReplaceToUnity(UnityEditor.MenuCommand command) { var comp = command.context as XericUIrAttributeTextMeshProInputField; diff --git a/Runtime/OverrideUI/XericUIrAttributeTextMeshProText.cs b/Runtime/OverrideUI/XericUIrAttributeTextMeshProText.cs index 07b103d..f4fd0aa 100644 --- a/Runtime/OverrideUI/XericUIrAttributeTextMeshProText.cs +++ b/Runtime/OverrideUI/XericUIrAttributeTextMeshProText.cs @@ -56,7 +56,7 @@ namespace XericUI.OverrideUI #region 组件替换 #if UNITY_EDITOR - [UnityEditor.MenuItem("CONTEXT/TextMeshProUGUI/Replace To XericUIrAttributeTextMeshProText", true)] + [UnityEditor.MenuItem("CONTEXT/TextMeshProUGUI/Replace To/Xeric (Attribute)", true)] private static bool ValidateReplaceToAttribute(UnityEditor.MenuCommand command) { #if dUI_TextMeshPro @@ -66,7 +66,7 @@ namespace XericUI.OverrideUI #endif } - [UnityEditor.MenuItem("CONTEXT/TextMeshProUGUI/Replace To XericUIrAttributeTextMeshProText", false, 10)] + [UnityEditor.MenuItem("CONTEXT/TextMeshProUGUI/Replace To/Xeric (Attribute)", false, 10)] private static void ReplaceToAttribute(UnityEditor.MenuCommand command) { #if dUI_TextMeshPro @@ -76,11 +76,11 @@ namespace XericUI.OverrideUI #endif } - [UnityEditor.MenuItem("CONTEXT/TextMeshProUGUI/Replace To TextMeshProUGUI", true)] + [UnityEditor.MenuItem("CONTEXT/TextMeshProUGUI/Replace To/Unity TextMeshPro", true)] private static bool ValidateReplaceToUnity(UnityEditor.MenuCommand command) => command.context is XericUIrAttributeTextMeshProText; - [UnityEditor.MenuItem("CONTEXT/TextMeshProUGUI/Replace To TextMeshProUGUI", false, 10)] + [UnityEditor.MenuItem("CONTEXT/TextMeshProUGUI/Replace To/Unity TextMeshPro", false, 100)] private static void ReplaceToUnity(UnityEditor.MenuCommand command) { var comp = command.context as XericUIrAttributeTextMeshProText; diff --git a/Runtime/OverrideUI/XericUIrAttributeToggle.cs b/Runtime/OverrideUI/XericUIrAttributeToggle.cs index fa1f6fd..99141d6 100644 --- a/Runtime/OverrideUI/XericUIrAttributeToggle.cs +++ b/Runtime/OverrideUI/XericUIrAttributeToggle.cs @@ -26,11 +26,11 @@ namespace XericUI.OverrideUI #region 组件替换 #if UNITY_EDITOR - [UnityEditor.MenuItem("CONTEXT/Toggle/Replace To XericUIrAttributeToggle", true)] + [UnityEditor.MenuItem("CONTEXT/Toggle/Replace To/Xeric (Attribute)", true)] private static bool ValidateReplaceToAttribute(UnityEditor.MenuCommand command) => command.context is Toggle and not XericUIrAttributeToggle; - [UnityEditor.MenuItem("CONTEXT/Toggle/Replace To XericUIrAttributeToggle", false, 11)] + [UnityEditor.MenuItem("CONTEXT/Toggle/Replace To/Xeric (Attribute)", false, 12)] private static void ReplaceToAttribute(UnityEditor.MenuCommand command) => command.ReplaceCommandContext( @@ -42,11 +42,11 @@ namespace XericUI.OverrideUI t.onValueChanged = d.onValueChanged; }); - [UnityEditor.MenuItem("CONTEXT/Toggle/Replace To Toggle", true)] + [UnityEditor.MenuItem("CONTEXT/Toggle/Replace To/Unity Toggle", true)] private static bool ValidateReplaceToUnity(UnityEditor.MenuCommand command) => command.context is XericUIrAttributeToggle; - [UnityEditor.MenuItem("CONTEXT/Toggle/Replace To Toggle", false, 11)] + [UnityEditor.MenuItem("CONTEXT/Toggle/Replace To/Unity Toggle", false, 100)] private static void ReplaceToUnity(UnityEditor.MenuCommand command) => command.ReplaceCommandContext( diff --git a/Runtime/XTable.meta b/Runtime/XTable.meta new file mode 100644 index 0000000..7a9fc76 --- /dev/null +++ b/Runtime/XTable.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 758008eeefb9f0540b0dce989b56c968 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/XTable/Core.meta b/Runtime/XTable/Core.meta new file mode 100644 index 0000000..e3cf74b --- /dev/null +++ b/Runtime/XTable/Core.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 186fffef629f713499774b802be4f587 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/XTable/Core/XTableBlock.cs b/Runtime/XTable/Core/XTableBlock.cs new file mode 100644 index 0000000..8165fdd --- /dev/null +++ b/Runtime/XTable/Core/XTableBlock.cs @@ -0,0 +1,90 @@ +using System.Collections.Generic; + +namespace XericUI.XTable.Core +{ + /// + /// 单个数据块——保存 BlockSizeX × BlockSizeY 个单元格数据。 + /// 数据使用线性数组存储,索引 = localRow * BlockSizeX + localCol。 + /// + public class XTableBlock + { + /// 块在全局块坐标系中的行号 + public int BlockRow; + + /// 块在全局块坐标系中的列号 + public int BlockCol; + + /// 块内单元格列数 + public int BlockSizeX; + + /// 块内单元格行数 + public int BlockSizeY; + + /// + /// 数据存储——线性数组。 + /// 索引 = localRow * BlockSizeX + localCol。 + /// + public XTableCellData[] Cells; + + /// 合并单元格描述列表 + public List MergeDescriptors; + + /// + /// 创建指定尺寸的块 + /// + public XTableBlock(int blockSizeX, int blockSizeY) + { + BlockSizeX = blockSizeX; + BlockSizeY = blockSizeY; + Cells = new XTableCellData[blockSizeX * blockSizeY]; + MergeDescriptors = new List(); + } + + /// + /// 创建指定尺寸和位置的块 + /// + public XTableBlock(int blockSizeX, int blockSizeY, int blockRow, int blockCol) + : this(blockSizeX, blockSizeY) + { + BlockRow = blockRow; + BlockCol = blockCol; + } + + /// + /// 通过块内局部坐标获取单元格数据 + /// + public XTableCellData GetCell(int localRow, int localCol) + { + int index = localRow * BlockSizeX + localCol; + if (index < 0 || index >= Cells.Length) return null; + return Cells[index]; + } + + /// + /// 通过块内局部坐标设置单元格数据 + /// + public void SetCell(int localRow, int localCol, XTableCellData data) + { + int index = localRow * BlockSizeX + localCol; + if (index < 0 || index >= Cells.Length) return; + Cells[index] = data; + } + + /// + /// 检查是否有合并描述重定向此单元格 + /// + /// 合并描述,若无重定向则返回 null + public XTableMergeDescriptor GetMergeRedirect(int localRow, int localCol) + { + if (MergeDescriptors == null || MergeDescriptors.Count == 0) + return null; + + for (int i = 0; i < MergeDescriptors.Count; i++) + { + if (MergeDescriptors[i].Contains(localRow, localCol)) + return MergeDescriptors[i]; + } + return null; + } + } +} diff --git a/Runtime/XTable/Core/XTableBlock.cs.meta b/Runtime/XTable/Core/XTableBlock.cs.meta new file mode 100644 index 0000000..9464efa --- /dev/null +++ b/Runtime/XTable/Core/XTableBlock.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: aa9a14d1a32e7fe4f95c29a0c18deddd +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/XTable/Core/XTableCellData.cs b/Runtime/XTable/Core/XTableCellData.cs new file mode 100644 index 0000000..00dc7db --- /dev/null +++ b/Runtime/XTable/Core/XTableCellData.cs @@ -0,0 +1,48 @@ +using System; + +using UnityEngine; + +namespace XericUI.XTable.Core +{ + /// + /// 单元格数据模型——仅保存数据内容,不保存自身尺寸。 + /// 尺寸由渲染阶段的行列标题(行高/列宽)决定。 + /// 样式通过 StyleNamespace 字符串引用 StyleManager 中的命名空间。 + /// + [Serializable] + public class XTableCellData + { + /// 文本内容 + public string Text; + + /// 图片纹理 + public Texture2D Image; + + /// 预制体对象(用于嵌入复杂 UI 元素) + public GameObject Prefab; + + /// + /// 样式命名空间——对应 StyleManager 中的命名空间。 + /// 为空或 "xeric_table_default" 时使用默认表格样式。 + /// 渲染时从此命名空间读取 /cell/bg/color、/cell/font/size 等路径。 + /// + public string StyleNamespace; + + public XTableCellData() { } + + public XTableCellData(string text) + { + Text = text; + } + + public XTableCellData(string text, string styleNamespace = null) : this(text) + { + StyleNamespace = styleNamespace; + } + + public XTableCellData(string text, Texture2D image, string styleNamespace = null) : this(text, styleNamespace) + { + Image = image; + } + } +} diff --git a/Runtime/XTable/Core/XTableCellData.cs.meta b/Runtime/XTable/Core/XTableCellData.cs.meta new file mode 100644 index 0000000..e2cbdfa --- /dev/null +++ b/Runtime/XTable/Core/XTableCellData.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: e783197e3dfc5a5489b99e27504e9cf3 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/XTable/Core/XTableData.cs b/Runtime/XTable/Core/XTableData.cs new file mode 100644 index 0000000..6658399 --- /dev/null +++ b/Runtime/XTable/Core/XTableData.cs @@ -0,0 +1,247 @@ +using System.Collections.Generic; + +using XericUI.XTable.Mapping; + +namespace XericUI.XTable.Core +{ + /// + /// 表格数据主类——持有块字典,提供单元格级别的读写接口。 + /// 块按需创建(SetCell 时自动创建不存在的块),通过 Z 曲线索引映射。 + /// + public class XTableData + { + #region 属性 + + /// 块内列数(默认 32) + public int BlockSizeX { get; private set; } + + /// 块内行数(默认 32) + public int BlockSizeY { get; private set; } + + /// + /// 块字典——key = Z 曲线索引(ulong),value = 数据块。 + /// 具有哈希表性质,按需创建,键不连续(离散存储)。 + /// + public Dictionary BlockMap { get; private set; } + + #endregion + + #region 构造函数 + + /// + /// 创建表格数据实例 + /// + /// 块内列数,默认 32 + /// 块内行数,默认 32 + public XTableData(int blockSizeX = 32, int blockSizeY = 32) + { + BlockSizeX = blockSizeX > 0 ? blockSizeX : 32; + BlockSizeY = blockSizeY > 0 ? blockSizeY : 32; + BlockMap = new Dictionary(); + } + + #endregion + + #region 核心访问方法 + + /// + /// 通过全局行列坐标获取单元格数据。 + /// 先计算块坐标和 Z 索引,查找块字典,再检查合并重定向。 + /// + /// 单元格数据,不存在则返回 null + public XTableCellData GetCell(int row, int col) + { + XTableCoordinateUtility.CellToBlockCoordinate( + row, col, BlockSizeX, BlockSizeY, + out int blockRow, out int blockCol, + out int localRow, out int localCol); + + ulong zIndex = XTableBlockMapping.BlockCoordToZIndex(blockRow, blockCol); + + if (!BlockMap.TryGetValue(zIndex, out XTableBlock block)) + return null; + + // 检查合并重定向 + XTableMergeDescriptor merge = block.GetMergeRedirect(localRow, localCol); + if (merge != null) + { + merge.GetRedirectTarget(out ulong targetBlockIndex, out int targetRow, out int targetCol); + + if (merge.IsCrossBlock) + { + // 跨块引用:跳转到目标块 + if (!BlockMap.TryGetValue(targetBlockIndex, out XTableBlock targetBlock)) + return null; + return targetBlock.GetCell(targetRow, targetCol); + } + else + { + // 本地引用:同块内重定向 + return block.GetCell(targetRow, targetCol); + } + } + + return block.GetCell(localRow, localCol); + } + + /// + /// 尝试获取单元格数据 + /// + public bool TryGetCell(int row, int col, out XTableCellData data) + { + data = GetCell(row, col); + return data != null; + } + + /// + /// 通过全局行列坐标设置单元格数据。 + /// 如果对应的块不存在,则按需创建。 + /// + public void SetCell(int row, int col, XTableCellData data) + { + XTableCoordinateUtility.CellToBlockCoordinate( + row, col, BlockSizeX, BlockSizeY, + out int blockRow, out int blockCol, + out int localRow, out int localCol); + + ulong zIndex = XTableBlockMapping.BlockCoordToZIndex(blockRow, blockCol); + + if (!BlockMap.TryGetValue(zIndex, out XTableBlock block)) + { + block = new XTableBlock(BlockSizeX, BlockSizeY, blockRow, blockCol); + BlockMap[zIndex] = block; + } + + block.SetCell(localRow, localCol, data); + } + + #endregion + + #region 合并单元格 + + /// + /// 合并从 (startRow, startCol) 开始、跨越 rowSpan 行和 colSpan 列的矩形区域。 + /// 合并源为左上角单元格 (startRow, startCol)。 + /// 如果合并区域跨越多个块,则每个涉及的块都会创建对应的 MergeDescriptor。 + /// + public void MergeCells(int startRow, int startCol, int rowSpan, int colSpan) + { + // 确保合并源单元格存在 + XTableCoordinateUtility.CellToBlockCoordinate( + startRow, startCol, BlockSizeX, BlockSizeY, + out int srcBlockRow, out int srcBlockCol, + out int srcLocalRow, out int srcLocalCol); + + ulong srcZIndex = XTableBlockMapping.BlockCoordToZIndex(srcBlockRow, srcBlockCol); + + // 遍历合并区域内的所有单元格,分块处理 + for (int r = startRow; r < startRow + rowSpan; r++) + { + for (int c = startCol; c < startCol + colSpan; c++) + { + // 跳过合并源自身 + if (r == startRow && c == startCol) continue; + + XTableCoordinateUtility.CellToBlockCoordinate( + r, c, BlockSizeX, BlockSizeY, + out int blockRow, out int blockCol, + out int localRow, out int localCol); + + ulong zIndex = XTableBlockMapping.BlockCoordToZIndex(blockRow, blockCol); + + // 确保块存在 + if (!BlockMap.TryGetValue(zIndex, out XTableBlock block)) + { + block = new XTableBlock(BlockSizeX, BlockSizeY, blockRow, blockCol); + BlockMap[zIndex] = block; + } + + // 查找或创建此块的合并描述(相同源) + XTableMergeDescriptor descriptor = FindOrCreateMergeDescriptor(block, + zIndex == srcZIndex ? srcLocalRow : -1, + zIndex == srcZIndex ? srcLocalCol : -1, + srcZIndex, srcLocalRow, srcLocalCol, + zIndex != srcZIndex); + + descriptor.MergedCellSet.Add((localRow, localCol)); + } + } + } + + /// + /// 查找或创建合并描述 + /// + private XTableMergeDescriptor FindOrCreateMergeDescriptor( + XTableBlock block, int checkLocalRow, int checkLocalCol, + ulong srcZIndex, int srcLocalRow, int srcLocalCol, bool isCrossBlock) + { + // 查找已有描述 + for (int i = 0; i < block.MergeDescriptors.Count; i++) + { + var desc = block.MergeDescriptors[i]; + if (desc.MergeRowSpan > 0 && desc.MergeColSpan > 0) + { + // 简化的同源判断:跨块引用共享 sourceBlockIndex + if (isCrossBlock && desc.IsCrossBlock && desc.SourceBlockIndex == srcZIndex) + return desc; + } + } + + // 创建新描述 + var newDesc = new XTableMergeDescriptor + { + IsCrossBlock = isCrossBlock, + SourceBlockIndex = srcZIndex, + SourceLocalRow = srcLocalRow, + SourceLocalCol = srcLocalCol + }; + block.MergeDescriptors.Add(newDesc); + return newDesc; + } + + /// + /// 取消指定区域的合并 + /// + public void UnmergeCells(int startRow, int startCol) + { + XTableCoordinateUtility.CellToBlockCoordinate( + startRow, startCol, BlockSizeX, BlockSizeY, + out int blockRow, out int blockCol, + out int localRow, out int localCol); + + ulong zIndex = XTableBlockMapping.BlockCoordToZIndex(blockRow, blockCol); + + if (!BlockMap.TryGetValue(zIndex, out XTableBlock block)) + return; + + // 移除包含此单元格的所有合并描述 + for (int i = block.MergeDescriptors.Count - 1; i >= 0; i--) + { + if (block.MergeDescriptors[i].Contains(localRow, localCol)) + { + block.MergeDescriptors.RemoveAt(i); + } + } + } + + #endregion + + #region 查询方法 + + /// + /// 判断指定块是否存在 + /// + public bool HasBlock(int blockRow, int blockCol) + { + ulong zIndex = XTableBlockMapping.BlockCoordToZIndex(blockRow, blockCol); + return BlockMap.ContainsKey(zIndex); + } + + /// + /// 获取块数量 + /// + public int BlockCount => BlockMap.Count; + + #endregion + } +} diff --git a/Runtime/XTable/Core/XTableData.cs.meta b/Runtime/XTable/Core/XTableData.cs.meta new file mode 100644 index 0000000..b1416ad --- /dev/null +++ b/Runtime/XTable/Core/XTableData.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 2e5d95e8d6d3d144d8fff6e41f175d77 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/XTable/Core/XTableMergeDescriptor.cs b/Runtime/XTable/Core/XTableMergeDescriptor.cs new file mode 100644 index 0000000..0016ac5 --- /dev/null +++ b/Runtime/XTable/Core/XTableMergeDescriptor.cs @@ -0,0 +1,67 @@ +using System; +using System.Collections.Generic; + +namespace XericUI.XTable.Core +{ + /// + /// 合并单元格描述——所有被合并单元格的 id 指向合并源(左上角单元格)。 + /// 同一类型支持本地块内引用和跨块引用,通过 区分。 + /// + [Serializable] + public class XTableMergeDescriptor + { + /// 合并区域在本地块内的起始行 + public int LocalStartRow; + + /// 合并区域在本地块内的起始列 + public int LocalStartCol; + + /// 合并跨越的行数 + public int MergeRowSpan; + + /// 合并跨越的列数 + public int MergeColSpan; + + /// 是否为跨块引用(true=合并源在另一个块上) + public bool IsCrossBlock; + + /// + /// 跨块引用时:源块在字典中的 key(Z 曲线索引) + /// 本地引用时:忽略此字段 + /// + public ulong SourceBlockIndex; + + /// 源单元格在块内的行号(跨块时=源块的行、本地时=本块的行) + public int SourceLocalRow; + + /// 源单元格在块内的列号(跨块时=源块的列、本地时=本块的列) + public int SourceLocalCol; + + /// 本块内被合并的 (row, col) 集合,查询时快速判断是否需要重定向 + public HashSet<(int row, int col)> MergedCellSet; + + public XTableMergeDescriptor() + { + MergedCellSet = new HashSet<(int, int)>(); + } + + /// + /// 判断指定本地坐标是否在此合并区域内 + /// + public bool Contains(int localRow, int localCol) + { + return MergedCellSet.Contains((localRow, localCol)); + } + + /// + /// 获取重定向目标——返回 (目标块Z索引, 目标块内行, 目标块内列) + /// 本地引用时目标块索引为 0(由调用方忽略) + /// + public void GetRedirectTarget(out ulong targetBlockIndex, out int targetRow, out int targetCol) + { + targetBlockIndex = IsCrossBlock ? SourceBlockIndex : 0; + targetRow = SourceLocalRow; + targetCol = SourceLocalCol; + } + } +} diff --git a/Runtime/XTable/Core/XTableMergeDescriptor.cs.meta b/Runtime/XTable/Core/XTableMergeDescriptor.cs.meta new file mode 100644 index 0000000..c0b65ee --- /dev/null +++ b/Runtime/XTable/Core/XTableMergeDescriptor.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: abe5a0c5395fa9249a66cd7195a49dbf +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/XTable/Mapping.meta b/Runtime/XTable/Mapping.meta new file mode 100644 index 0000000..5a5ca78 --- /dev/null +++ b/Runtime/XTable/Mapping.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: ac372aa4265804648bcff4f505b39f24 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/XTable/Mapping/XTableBlockMapping.cs b/Runtime/XTable/Mapping/XTableBlockMapping.cs new file mode 100644 index 0000000..e98d82e --- /dev/null +++ b/Runtime/XTable/Mapping/XTableBlockMapping.cs @@ -0,0 +1,38 @@ +using System.Runtime.CompilerServices; + +using XericLibrary.Runtime.MacroLibrary; + +namespace XericUI.XTable.Mapping +{ + /// + /// 块映射工具——将块坐标与 Z 曲线索引进行相互转换。 + /// 内部调用 的 Morton Code 编码/解码方法。 + /// Z 曲线索引避免纯横向排列产生的空缺问题,提升空间局部性。 + /// + public static class XTableBlockMapping + { + /// + /// 块坐标 → Z 曲线索引 (Morton Code) + /// + /// 块行号 (非负) + /// 块列号 (非负) + /// 64 位 Z 曲线索引,用作块字典的 key + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ulong BlockCoordToZIndex(int blockRow, int blockCol) + { + return MacroCurveMapping.ZOrderEncode(blockCol, blockRow); + } + + /// + /// Z 曲线索引 → 块坐标 + /// + /// Z 曲线索引 + /// 解码后的块行号 + /// 解码后的块列号 + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ZIndexToBlockCoord(ulong zIndex, out int blockRow, out int blockCol) + { + MacroCurveMapping.ZOrderDecode(zIndex, out blockCol, out blockRow); + } + } +} diff --git a/Runtime/XTable/Mapping/XTableBlockMapping.cs.meta b/Runtime/XTable/Mapping/XTableBlockMapping.cs.meta new file mode 100644 index 0000000..a2a1e14 --- /dev/null +++ b/Runtime/XTable/Mapping/XTableBlockMapping.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 6d2cad58624b7724fb43fdeca3aa2b50 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/XTable/Mapping/XTableCoordinateUtility.cs b/Runtime/XTable/Mapping/XTableCoordinateUtility.cs new file mode 100644 index 0000000..c10e59e --- /dev/null +++ b/Runtime/XTable/Mapping/XTableCoordinateUtility.cs @@ -0,0 +1,79 @@ +#if ENABLE_BURST +using Unity.Burst; +#endif + +using System.Runtime.CompilerServices; + +namespace XericUI.XTable.Mapping +{ + /// + /// 表格坐标计算工具——提供单元格坐标 ↔ 块坐标 的转换方法。 + /// 所有方法支持 Burst 编译加速。 + /// +#if ENABLE_BURST + [BurstCompile] +#endif + public static class XTableCoordinateUtility + { + /// + /// 将全局单元格坐标 (row, col) 转换为 块坐标 + 块内局部坐标。 + /// blockRow = row / blockSizeY, blockCol = col / blockSizeX + /// localRow = row % blockSizeY, localCol = col % blockSizeX + /// +#if ENABLE_BURST + [BurstCompile] +#endif + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void CellToBlockCoordinate( + int row, int col, int blockSizeX, int blockSizeY, + out int blockRow, out int blockCol, + out int localRow, out int localCol) + { + blockRow = row / blockSizeY; + blockCol = col / blockSizeX; + localRow = row % blockSizeY; + localCol = col % blockSizeX; + } + + /// + /// 计算块内的线性索引。 + /// index = localRow * blockSizeX + localCol + /// +#if ENABLE_BURST + [BurstCompile] +#endif + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int LocalCellIndex(int localRow, int localCol, int blockSizeX) + { + return localRow * blockSizeX + localCol; + } + + /// + /// 从块内线性索引反算局部行列坐标。 + /// +#if ENABLE_BURST + [BurstCompile] +#endif + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void LocalCellFromIndex(int index, int blockSizeX, + out int localRow, out int localCol) + { + localRow = index / blockSizeX; + localCol = index % blockSizeX; + } + + /// + /// 从全局行列坐标直接计算块内线性索引。 + /// +#if ENABLE_BURST + [BurstCompile] +#endif + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int GlobalCellToLocalIndex(int row, int col, int blockSizeX, int blockSizeY) + { + int localRow = row % blockSizeY; + int localCol = col % blockSizeX; + return localRow * blockSizeX + localCol; + } + } +} diff --git a/Runtime/XTable/Mapping/XTableCoordinateUtility.cs.meta b/Runtime/XTable/Mapping/XTableCoordinateUtility.cs.meta new file mode 100644 index 0000000..0120d1b --- /dev/null +++ b/Runtime/XTable/Mapping/XTableCoordinateUtility.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 45c10335c21cdf342816fd78d61cec09 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/XTable/Rendering.meta b/Runtime/XTable/Rendering.meta new file mode 100644 index 0000000..946d29c --- /dev/null +++ b/Runtime/XTable/Rendering.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 3b7ab03cb8f1d894a92f9ef988cb9245 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/XTable/Rendering/Component.meta b/Runtime/XTable/Rendering/Component.meta new file mode 100644 index 0000000..b8dd7d9 --- /dev/null +++ b/Runtime/XTable/Rendering/Component.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: e7813b6f36b85854b8bfcae0874499d1 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/XTable/Rendering/Component/XTableDirtyFlag.cs b/Runtime/XTable/Rendering/Component/XTableDirtyFlag.cs new file mode 100644 index 0000000..8a6d5aa --- /dev/null +++ b/Runtime/XTable/Rendering/Component/XTableDirtyFlag.cs @@ -0,0 +1,55 @@ +using System; + +namespace XericUI.XTable.Rendering.Component +{ + /// + /// 脏标记类型——标识表格需要刷新的变更类型。 + /// + [Flags] + public enum TableDirtyType + { + None = 0, + /// 数据内容变更 + DataChanged = 1 << 0, + /// 视图范围变更(滚动/缩放) + ViewChanged = 1 << 1, + /// 样式表变更 + StyleChanged = 1 << 2, + /// 选中状态变更 + SelectionChanged = 1 << 3, + /// 行列尺寸变更 + LayoutChanged = 1 << 4, + /// 全部脏(强制完整刷新) + All = DataChanged | ViewChanged | StyleChanged | SelectionChanged | LayoutChanged + } + + /// + /// 脏标记结构——追踪表格的刷新状态。 + /// + public struct XTableDirtyFlag + { + /// 当前脏标记集合 + public TableDirtyType Flags; + + /// 是否有任何脏标记 + public bool IsDirty => Flags != TableDirtyType.None; + + /// 标记一种脏类型 + public void Mark(TableDirtyType type) + { + Flags |= type; + } + + /// 清除所有脏标记 + public void Clear() + { + Flags = TableDirtyType.None; + } + + /// 检查是否包含特定脏类型 + public bool Has(TableDirtyType type) + { + return (Flags & type) == type; + } + } +} diff --git a/Runtime/XTable/Rendering/Component/XTableDirtyFlag.cs.meta b/Runtime/XTable/Rendering/Component/XTableDirtyFlag.cs.meta new file mode 100644 index 0000000..629f78a --- /dev/null +++ b/Runtime/XTable/Rendering/Component/XTableDirtyFlag.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 8f87a16b53fa84c4a97b8ffd44aa2868 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/XTable/Rendering/Component/XTableObjectPool.cs b/Runtime/XTable/Rendering/Component/XTableObjectPool.cs new file mode 100644 index 0000000..55adb70 --- /dev/null +++ b/Runtime/XTable/Rendering/Component/XTableObjectPool.cs @@ -0,0 +1,120 @@ +using System.Collections.Generic; + +using UnityEngine; + +namespace XericUI.XTable.Rendering.Component +{ + /// + /// 单元格对象池——复用表格单元格 GameObject,避免频繁 Instantiate/Destroy。 + /// + public class XTableObjectPool + { + /// 回收池 + private readonly Stack m_Pool = new Stack(); + + /// 当前活跃对象(已借出) + private readonly HashSet m_Active = new HashSet(); + + /// 预制体模板 + private readonly GameObject m_Prefab; + + /// 父级 Transform + private readonly Transform m_Parent; + + /// 初始池容量 + private readonly int m_InitialCapacity; + + public XTableObjectPool(GameObject prefab, Transform parent, int initialCapacity = 64) + { + m_Prefab = prefab; + m_Parent = parent; + m_InitialCapacity = initialCapacity; + + // 预创建对象 + for (int i = 0; i < initialCapacity; i++) + { + GameObject obj = CreateNew(); + obj.SetActive(false); + m_Pool.Push(obj); + } + } + + /// + /// 从池中获取一个对象 + /// + public GameObject Get() + { + GameObject obj; + if (m_Pool.Count > 0) + { + obj = m_Pool.Pop(); + } + else + { + obj = CreateNew(); + } + + obj.SetActive(true); + m_Active.Add(obj); + return obj; + } + + /// + /// 将对象归还到池中 + /// + public void Return(GameObject obj) + { + if (obj == null) return; + + obj.SetActive(false); + m_Active.Remove(obj); + m_Pool.Push(obj); + } + + /// + /// 归还所有活跃对象 + /// + public void ReturnAll() + { + foreach (var obj in m_Active) + { + if (obj != null) + { + obj.SetActive(false); + m_Pool.Push(obj); + } + } + m_Active.Clear(); + } + + /// + /// 清理所有对象 + /// + public void Clear() + { + ReturnAll(); + while (m_Pool.Count > 0) + { + var obj = m_Pool.Pop(); + if (obj != null) + Object.Destroy(obj); + } + } + + /// 当前活跃对象数 + public int ActiveCount => m_Active.Count; + + /// 池中空闲对象数 + public int PoolCount => m_Pool.Count; + + private GameObject CreateNew() + { + GameObject obj = m_Prefab != null + ? Object.Instantiate(m_Prefab, m_Parent) + : new GameObject("TableCell", typeof(RectTransform)); + + obj.transform.SetParent(m_Parent, false); + return obj; + } + } +} diff --git a/Runtime/XTable/Rendering/Component/XTableObjectPool.cs.meta b/Runtime/XTable/Rendering/Component/XTableObjectPool.cs.meta new file mode 100644 index 0000000..98119e7 --- /dev/null +++ b/Runtime/XTable/Rendering/Component/XTableObjectPool.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: ae4a5754316035e4489de05e40389c9e +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/XTable/Rendering/Component/XericUIActionTable.cs b/Runtime/XTable/Rendering/Component/XericUIActionTable.cs new file mode 100644 index 0000000..f82c54f --- /dev/null +++ b/Runtime/XTable/Rendering/Component/XericUIActionTable.cs @@ -0,0 +1,572 @@ +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.Rendering.Elements; +using XericLibrary.Runtime.SuperStyleSheet; + +namespace XericUI.XTable.Rendering.Component +{ + /// + /// 表格组件——纯数据渲染器。 + /// 一张全幅背景 + 按需拼装单元格文本/图片。 + /// 无行列标题、无样式差异分割线——所有内容由数据定义。 + /// + [ExecuteAlways] + [AddComponentMenu("Xeric UI Vessel/Table/Xeric UI Action Table", 60)] + public class XericUIActionTable : XericUIBehaciour, IPointerClickHandler + { + #region 常量 + + private const string DEFAULT_STYLE_NS = "xeric_table_default"; + + #endregion + + #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 = DEFAULT_STYLE_NS; + + [SerializeField] private ScrollRect m_ScrollRect; + + #endregion + + #region 私有字段 + + [System.NonSerialized] private CellAssembler m_Assembler; + [System.NonSerialized] private XTableDirtyFlag m_DirtyFlag; + + private int m_SelectedRow = -1; + private int m_SelectedCol = -1; + [System.NonSerialized] private GameObject m_BackgroundGo; + + private float m_TotalWidth; + private float m_TotalHeight; + + // ScrollRect 可见范围 + private Rect m_VisibleRect; + + #endregion + + #region 事件 + + public event Action OnCellSelected; + public event Action OnTableRefreshed; + + #endregion + + #region 公开属性 + + public XTableData TableData + { + get => m_TableData; + set { m_TableData = value; MarkDirty(TableDirtyType.DataChanged); } + } + + public float[] RowHeights => m_RowHeights; + public float[] ColWidths => m_ColWidths; + + public string DefaultStyleNamespace => m_DefaultStyleNamespace; + public int SelectedRow => m_SelectedRow; + public int SelectedCol => m_SelectedCol; + + #endregion + + #region 生命周期 + + protected override void Awake() + { + base.Awake(); + + if (m_TableData == null) + m_TableData = new XTableData(); + + m_Assembler = new CellAssembler(rectTransform); + RecalculateTotalSize(); + EnsureDefaultStyleNamespace(); + CreateDefaultSampleData(); + CreateBackground(); + + if (m_ScrollRect != null) + m_ScrollRect.onValueChanged.AddListener(OnScrollValueChanged); + + if (!Application.isPlaying) + SubscribeEditorUpdate(); + + MarkDirty(TableDirtyType.All); + } + + protected override void OnEnable() + { + base.OnEnable(); + EnsureDefaultStyleNamespace(); + MarkDirty(TableDirtyType.All); + } + + 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.DestroyAll(); + if (m_BackgroundGo != null) + { + if (Application.isPlaying) Destroy(m_BackgroundGo); + else DestroyImmediate(m_BackgroundGo); + m_BackgroundGo = null; + } + + base.OnDestroy(); + } + + private void Update() + { + if (m_DirtyFlag.IsDirty) RefreshView(); + } + + #endregion + + #region 默认示例数据 + + private bool m_DefaultDataCreated; + + private void CreateDefaultSampleData() + { + if (m_DefaultDataCreated) return; + m_DefaultDataCreated = true; + + 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++) + SetCellTextInternal(r, c, sample[r, c], skipDirty: true); + } + + private void SetCellTextInternal(int row, int col, string text, bool skipDirty) + { + var data = m_TableData.GetCell(row, col); + if (data == null) + { + data = new XTableCellData(); + m_TableData.SetCell(row, col, data); + } + data.Text = text; + if (!skipDirty) MarkDirty(TableDirtyType.DataChanged); + } + + #endregion + + #region 编辑器模式支持 + + [System.NonSerialized] private bool m_EditorUpdateSubscribed; + + private void SubscribeEditorUpdate() + { + if (m_EditorUpdateSubscribed) return; +#if UNITY_EDITOR + UnityEditor.EditorApplication.update += EditorUpdate; + m_EditorUpdateSubscribed = true; +#endif + } + + private void UnsubscribeEditorUpdate() + { + if (!m_EditorUpdateSubscribed) return; +#if UNITY_EDITOR + UnityEditor.EditorApplication.update -= EditorUpdate; + m_EditorUpdateSubscribed = false; +#endif + } + +#if UNITY_EDITOR + private void EditorUpdate() + { + if (this == null || !isActiveAndEnabled) + { + UnsubscribeEditorUpdate(); + return; + } + if (m_DirtyFlag.IsDirty) + RefreshView(); + } +#endif + + #endregion + + #region 公开方法 + + public void SetCellData(int row, int col, XTableCellData data) + { + m_TableData.SetCell(row, col, data); + MarkDirty(TableDirtyType.DataChanged); + } + + public void SetCellText(int row, int col, string text) + { + SetCellTextInternal(row, col, text, skipDirty: false); + } + + public XTableCellData GetCellData(int row, int col) + => m_TableData?.GetCell(row, col); + + public void MergeCells(int startRow, int startCol, int rowSpan, int colSpan) + { + m_TableData.MergeCells(startRow, startCol, rowSpan, colSpan); + MarkDirty(TableDirtyType.DataChanged); + } + + public void UnmergeCells(int startRow, int startCol) + { + m_TableData.UnmergeCells(startRow, startCol); + MarkDirty(TableDirtyType.DataChanged); + } + + public void SelectCell(int row, int col) + { + m_SelectedRow = row; + m_SelectedCol = col; + MarkDirty(TableDirtyType.DataChanged); + } + + public void ClearSelection() + { + m_SelectedRow = -1; + m_SelectedCol = -1; + MarkDirty(TableDirtyType.DataChanged); + } + + public void RefreshView() + { + if (m_TableData == null || m_Assembler == null) return; + if (m_RowHeights == null || m_ColWidths == null) return; + if (m_RowHeights.Length == 0 || m_ColWidths.Length == 0) return; + +#if UNITY_EDITOR + if (!Application.isPlaying) + Canvas.ForceUpdateCanvases(); +#endif + + RecalculateTotalSize(); + UpdateVisibleRect(); + + // 更新背景尺寸 + if (m_BackgroundGo != null) + { + var bgRt = m_BackgroundGo.GetComponent(); + bgRt.sizeDelta = new Vector2(m_TotalWidth, m_TotalHeight); + } + + // 回收池 + m_Assembler.ReturnAll(); + + // 计算可见范围 + int sr, er, sc, ec; + CalcVisibleRange(out sr, out er, out sc, out ec); + + if (er <= sr || ec <= sc) return; + + // 渲染每个可见单元格 + RenderCells(sr, er, sc, ec); + + m_DirtyFlag.Clear(); + OnTableRefreshed?.Invoke(); + } + + #endregion + + #region 背景 + + private void CreateBackground() + { + if (m_BackgroundGo != null) return; + + m_BackgroundGo = new GameObject("_TableBackground", + typeof(RectTransform), typeof(CanvasRenderer), typeof(Image)); + m_BackgroundGo.hideFlags = HideFlags.DontSave | HideFlags.HideInHierarchy; + m_BackgroundGo.transform.SetParent(rectTransform, false); + + RectTransform bgRt = m_BackgroundGo.GetComponent(); + bgRt.anchorMin = new Vector2(0, 1); + bgRt.anchorMax = new Vector2(0, 1); + bgRt.pivot = new Vector2(0, 1); + bgRt.anchoredPosition = Vector2.zero; + bgRt.sizeDelta = new Vector2(m_TotalWidth, m_TotalHeight); + + var bgImg = m_BackgroundGo.GetComponent(); + bgImg.color = new Color(0.18f, 0.18f, 0.18f, 1f); + bgImg.raycastTarget = false; + } + + /// 完全重建表格——销毁所有动态生成的对象并重新初始化 + [ContextMenu("Xeric UI/完全重建表格")] + public void FullRebuild() + { + m_Assembler?.DestroyAll(); + m_DefaultDataCreated = false; + + if (m_BackgroundGo != null) + { + if (Application.isPlaying) Destroy(m_BackgroundGo); + else DestroyImmediate(m_BackgroundGo); + m_BackgroundGo = null; + } + + m_TableData = new XTableData(); + + m_Assembler = new CellAssembler(rectTransform); + RecalculateTotalSize(); + CreateDefaultSampleData(); + CreateBackground(); + MarkDirty(TableDirtyType.All); + RefreshView(); + } + + #endregion + + #region 单元格渲染 + + private void RenderCells(int sr, int er, int sc, int ec) + { + Color fgColor = GetStyleColor(m_DefaultStyleNamespace, "/cell/fg/color", + new Color(0.05f, 0.05f, 0.05f)); + Color selectionColor = new Color(0.18f, 0.42f, 0.82f, 0.25f); + int fontSize = GetStyleInt(m_DefaultStyleNamespace, "/cell/font/size", 14); + + for (int r = sr; r < er && r < m_RowHeights.Length; r++) + { + for (int c = sc; c < ec && c < m_ColWidths.Length; c++) + { + float cellX = GetColLeftX(c); + float cellY = -GetRowTopY(r); + float cellW = m_ColWidths[c]; + float cellH = m_RowHeights[r]; + XTableCellData data = m_TableData.GetCell(r, c); + + // 选中高亮背景 + if (r == m_SelectedRow && c == m_SelectedCol) + { + var selImg = m_Assembler.GetImage("cell_select"); + selImg.Rect.anchoredPosition = new Vector2(cellX, cellY); + selImg.Rect.sizeDelta = new Vector2(cellW, cellH); + selImg.Image.color = selectionColor; + } + + // 水平网格线 + var hLine = m_Assembler.GetImage("cell_hline"); + hLine.Rect.anchoredPosition = new Vector2(cellX, cellY - cellH); + hLine.Rect.sizeDelta = new Vector2(cellW, 1f); + hLine.Image.color = new Color(0.35f, 0.35f, 0.35f, 0.5f); + + // 垂直网格线 + var vLine = m_Assembler.GetImage("cell_vline"); + vLine.Rect.anchoredPosition = new Vector2(cellX + cellW - 1, cellY); + vLine.Rect.sizeDelta = new Vector2(1f, cellH); + vLine.Image.color = new Color(0.35f, 0.35f, 0.35f, 0.5f); + + if (data == null) continue; + + // 文本:与 cell 同位置同尺寸,TMP 内部 Midline 对齐实现居中 + if (!string.IsNullOrEmpty(data.Text)) + { + var txt = m_Assembler.GetText("cell_text"); + txt.Rect.anchoredPosition = new Vector2(cellX + 4, cellY); + txt.Rect.sizeDelta = new Vector2(cellW - 8, cellH); + txt.Text.text = data.Text; + txt.Text.color = fgColor; + txt.Text.fontSize = fontSize; + txt.Text.alignment = TextAlignmentOptions.Midline; + } + } + } + } + + #endregion + + #region 坐标计算 + + private float GetRowTopY(int row) + { + float y = 0; + for (int i = 0; i < row && i < m_RowHeights.Length; i++) + y += m_RowHeights[i]; + return y; + } + + private float GetColLeftX(int col) + { + float x = 0; + for (int i = 0; i < col && i < m_ColWidths.Length; i++) + x += m_ColWidths[i]; + return x; + } + + private void RecalculateTotalSize() + { + m_TotalWidth = 0; + if (m_ColWidths != null) + foreach (float w in m_ColWidths) m_TotalWidth += w; + + m_TotalHeight = 0; + if (m_RowHeights != null) + foreach (float h in m_RowHeights) m_TotalHeight += h; + + if (m_ScrollRect != null && m_ScrollRect.content != null) + m_ScrollRect.content.sizeDelta = new Vector2(m_TotalWidth, m_TotalHeight); + } + + private void UpdateVisibleRect() + { + m_VisibleRect = new Rect(0, 0, m_TotalWidth, m_TotalHeight); + + if (m_ScrollRect != null && m_ScrollRect.viewport != null) + { + RectTransform vt = m_ScrollRect.viewport; + RectTransform ct = m_ScrollRect.content; + if (vt != null && ct != null) + { + float sx = Mathf.Max(0, -ct.anchoredPosition.x); + float sy = Mathf.Max(0, ct.anchoredPosition.y); + m_VisibleRect = new Rect(sx, sy, vt.rect.width, vt.rect.height); + } + } + } + + private void CalcVisibleRange(out int sr, out int er, out int sc, out int ec) + { + sr = 0; er = 0; sc = 0; ec = 0; + + float accY = 0; + for (int r = 0; r < m_RowHeights.Length; r++) + { + float rowBottom = accY + m_RowHeights[r]; + if (rowBottom > m_VisibleRect.y && accY < m_VisibleRect.yMax) + { + if (er == 0 && sr == 0) sr = r; + er = r + 1; + } + accY = rowBottom; + if (accY > m_VisibleRect.yMax) break; + } + + float accX = 0; + for (int c = 0; c < m_ColWidths.Length; c++) + { + float colRight = accX + m_ColWidths[c]; + if (colRight > m_VisibleRect.x && accX < m_VisibleRect.xMax) + { + if (ec == 0 && sc == 0) sc = c; + ec = c + 1; + } + accX = colRight; + if (accX > m_VisibleRect.xMax) break; + } + + if (sr < 0) sr = 0; + if (sc < 0) sc = 0; + } + + #endregion + + #region 样式 + + private Color GetStyleColor(string ns, string path, Color fallback) + { + StyleValue val = StyleManager.GetValue(ns, path); + return val != null ? (Color)val : fallback; + } + + private int GetStyleInt(string ns, string path, int fallback) + { + StyleValue val = StyleManager.GetValue(ns, path); + return val != null ? (int)val : fallback; + } + + private void EnsureDefaultStyleNamespace() + { + if (!StyleManager.HasStyle(m_DefaultStyleNamespace, "/cell/bg/color")) + { + StyleManager.AddDynamicStyle(m_DefaultStyleNamespace, "/cell/bg/color", + new Color(0.18f, 0.18f, 0.18f)); + StyleManager.AddDynamicStyle(m_DefaultStyleNamespace, "/cell/fg/color", + new Color(0.05f, 0.05f, 0.05f)); + StyleManager.AddDynamicStyle(m_DefaultStyleNamespace, "/cell/font/size", 14); + StyleManager.ForceRefresh(); + } + } + + #endregion + + #region 其他 + + private void MarkDirty(TableDirtyType type) => m_DirtyFlag.Mark(type); + + private void OnScrollValueChanged(Vector2 _) => MarkDirty(TableDirtyType.ViewChanged); + + #endregion + + #region IPointerClickHandler + + public void OnPointerClick(PointerEventData eventData) + { + RectTransformUtility.ScreenPointToLocalPointInRectangle( + rectTransform, eventData.position, eventData.pressEventCamera, out Vector2 localPoint); + + // rectTransform 锚点在左上角,localPoint 以中心为原点 + float tableX = localPoint.x - rectTransform.rect.xMin; + float tableY = rectTransform.rect.yMax - localPoint.y; + + float scrollY = m_ScrollRect != null ? Mathf.Max(0, m_ScrollRect.content.anchoredPosition.y) : 0; + float scrollX = m_ScrollRect != null ? Mathf.Max(0, -m_ScrollRect.content.anchoredPosition.x) : 0; + tableX += scrollX; + tableY += scrollY; + + int col = -1; + float ax = 0; + for (int c = 0; c < m_ColWidths.Length; c++) + { + if (tableX >= ax && tableX < ax + m_ColWidths[c]) { col = c; break; } + ax += m_ColWidths[c]; + } + + int row = -1; + float ay = 0; + for (int r = 0; r < m_RowHeights.Length; r++) + { + if (tableY >= ay && tableY < ay + m_RowHeights[r]) { row = r; break; } + ay += m_RowHeights[r]; + } + + if (row >= 0 && col >= 0) + { + SelectCell(row, col); + OnCellSelected?.Invoke(row, col); + } + } + + #endregion + } +} diff --git a/Runtime/XTable/Rendering/Component/XericUIActionTable.cs.meta b/Runtime/XTable/Rendering/Component/XericUIActionTable.cs.meta new file mode 100644 index 0000000..f73769c --- /dev/null +++ b/Runtime/XTable/Rendering/Component/XericUIActionTable.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 13944afb118d5de499192b64114ec4f2 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/XTable/Rendering/Elements.meta b/Runtime/XTable/Rendering/Elements.meta new file mode 100644 index 0000000..8496721 --- /dev/null +++ b/Runtime/XTable/Rendering/Elements.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 238b49eb1d61170498956f869ee62303 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/XTable/Rendering/Elements/CellAssembler.cs b/Runtime/XTable/Rendering/Elements/CellAssembler.cs new file mode 100644 index 0000000..3edb1ee --- /dev/null +++ b/Runtime/XTable/Rendering/Elements/CellAssembler.cs @@ -0,0 +1,268 @@ +using System.Collections.Generic; + +using TMPro; + +using UnityEngine; +using UnityEngine.UI; + +namespace XericUI.XTable.Rendering.Elements +{ + /// + /// 表格单元动态拼装器——通过对象池管理 Image + TMP_Text 的组合创建与回收。 + /// 所有元素统一使用 anchor=(0,1) pivot=(0,1) 左上角坐标系。 + /// 所有动态生成对象标记 HideFlags.DontSave | HideFlags.HideInHierarchy,防止泄漏到场景。 + /// + public class CellAssembler + { + #region 对象池定义 + + public class PooledImage + { + public GameObject Root; + public RectTransform Rect; + public Image Image; + public StyleBoundElement Style; + } + + public class PooledText + { + public GameObject Root; + public RectTransform Rect; + public TMP_Text Text; + public StyleBoundElement Style; + } + + #endregion + + #region 字段 + + private Transform m_Parent; + private Transform m_PoolRoot; + + private Stack m_ImagePool = new Stack(); + private Stack m_TextPool = new Stack(); + private List m_ActiveImages = new List(); + private List m_ActiveTexts = new List(); + + /// HideFlags 应用于所有动态创建的对象(不保存、不在 Hierarchy 显示) + private const HideFlags POOL_FLAGS = HideFlags.DontSave | HideFlags.HideInHierarchy; + + #endregion + + #region 构造函数与析构 + + public CellAssembler(Transform parent) + { + m_Parent = parent; + + GameObject poolRoot = new GameObject("_CellPool"); + poolRoot.hideFlags = POOL_FLAGS; + poolRoot.transform.SetParent(parent, false); + poolRoot.SetActive(false); + m_PoolRoot = poolRoot.transform; + } + + /// 彻底销毁所有池对象(包括活跃和休眠的) + public void DestroyAll() + { + // 杀死活跃对象 + for (int i = m_ActiveImages.Count - 1; i >= 0; i--) + { + KillObject(m_ActiveImages[i].Root); + } + m_ActiveImages.Clear(); + + for (int i = m_ActiveTexts.Count - 1; i >= 0; i--) + { + KillObject(m_ActiveTexts[i].Root); + } + m_ActiveTexts.Clear(); + + // 杀死池中对象 + while (m_ImagePool.Count > 0) + { + KillObject(m_ImagePool.Pop().Root); + } + while (m_TextPool.Count > 0) + { + KillObject(m_TextPool.Pop().Root); + } + + // 杀死池根 + if (m_PoolRoot != null) + { + KillObject(m_PoolRoot.gameObject); + m_PoolRoot = null; + } + } + + private static void KillObject(GameObject go) + { + if (go == null) return; + if (Application.isPlaying) + Object.Destroy(go); + else + Object.DestroyImmediate(go); + } + + #endregion + + #region 常量 + + private static readonly Vector2 ANCHOR_TOPLEFT_MIN = new Vector2(0, 1); + private static readonly Vector2 ANCHOR_TOPLEFT_MAX = new Vector2(0, 1); + private static readonly Vector2 PIVOT_TOPLEFT = new Vector2(0, 1); + + #endregion + + #region 池操作 + + public PooledImage GetImage(string name = "cell_img") + { + PooledImage item; + if (m_ImagePool.Count > 0) + { + item = m_ImagePool.Pop(); + item.Root.SetActive(true); + item.Root.transform.SetParent(m_Parent, false); + ResetImageDefaults(item); + } + else + { + item = CreateImage(name); + } + m_ActiveImages.Add(item); + return item; + } + + public PooledText GetText(string name = "cell_text") + { + PooledText item; + if (m_TextPool.Count > 0) + { + item = m_TextPool.Pop(); + item.Root.SetActive(true); + item.Root.transform.SetParent(m_Parent, false); + ResetTextDefaults(item); + } + else + { + item = CreateText(name); + } + m_ActiveTexts.Add(item); + return item; + } + + public void ReturnAll() + { + foreach (var img in m_ActiveImages) ReturnImage(img); + m_ActiveImages.Clear(); + + foreach (var txt in m_ActiveTexts) ReturnText(txt); + m_ActiveTexts.Clear(); + } + + private void ReturnImage(PooledImage item) + { + item.Style?.Unbind(); + item.Root.SetActive(false); + item.Root.transform.SetParent(m_PoolRoot, false); + m_ImagePool.Push(item); + } + + private void ReturnText(PooledText item) + { + item.Style?.Unbind(); + item.Text.text = ""; + item.Root.SetActive(false); + item.Root.transform.SetParent(m_PoolRoot, false); + m_TextPool.Push(item); + } + + #endregion + + #region 默认值重置 + + private void ResetImageDefaults(PooledImage item) + { + item.Rect.anchorMin = ANCHOR_TOPLEFT_MIN; + item.Rect.anchorMax = ANCHOR_TOPLEFT_MAX; + item.Rect.pivot = PIVOT_TOPLEFT; + item.Rect.anchoredPosition = Vector2.zero; + item.Rect.sizeDelta = Vector2.zero; + item.Image.color = Color.white; + item.Image.sprite = null; + item.Image.raycastTarget = false; + } + + private void ResetTextDefaults(PooledText item) + { + item.Rect.anchorMin = ANCHOR_TOPLEFT_MIN; + item.Rect.anchorMax = ANCHOR_TOPLEFT_MAX; + item.Rect.pivot = PIVOT_TOPLEFT; + item.Rect.anchoredPosition = Vector2.zero; + item.Rect.sizeDelta = Vector2.zero; + item.Text.text = ""; + item.Text.color = Color.black; + item.Text.fontSize = 14; + item.Text.alignment = TextAlignmentOptions.Midline; + item.Text.raycastTarget = false; + } + + #endregion + + #region 创建 + + private PooledImage CreateImage(string name) + { + GameObject go = new GameObject(name, + typeof(RectTransform), typeof(CanvasRenderer), typeof(Image)); + go.hideFlags = POOL_FLAGS; + go.transform.SetParent(m_Parent, false); + + Image img = go.GetComponent(); + img.raycastTarget = false; + + RectTransform rect = go.GetComponent(); + rect.anchorMin = ANCHOR_TOPLEFT_MIN; + rect.anchorMax = ANCHOR_TOPLEFT_MAX; + rect.pivot = PIVOT_TOPLEFT; + + return new PooledImage + { + Root = go, + Rect = rect, + Image = img, + Style = new StyleBoundElement { ImageComponent = img } + }; + } + + private PooledText CreateText(string name) + { + GameObject go = new GameObject(name, + typeof(RectTransform), typeof(CanvasRenderer), typeof(TextMeshProUGUI)); + go.hideFlags = POOL_FLAGS; + go.transform.SetParent(m_Parent, false); + + TMP_Text text = go.GetComponent(); + text.raycastTarget = false; + text.alignment = TextAlignmentOptions.Midline; + text.fontSize = 14; + + RectTransform rect = go.GetComponent(); + rect.anchorMin = ANCHOR_TOPLEFT_MIN; + rect.anchorMax = ANCHOR_TOPLEFT_MAX; + rect.pivot = PIVOT_TOPLEFT; + + return new PooledText + { + Root = go, + Rect = rect, + Text = text, + Style = new StyleBoundElement { TextComponent = text } + }; + } + + #endregion + } +} diff --git a/Runtime/XTable/Rendering/Elements/CellAssembler.cs.meta b/Runtime/XTable/Rendering/Elements/CellAssembler.cs.meta new file mode 100644 index 0000000..f3eb8a7 --- /dev/null +++ b/Runtime/XTable/Rendering/Elements/CellAssembler.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: c9dd186d27001684f897c8ca38815c74 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/XTable/Rendering/Elements/StyleBoundElement.cs b/Runtime/XTable/Rendering/Elements/StyleBoundElement.cs new file mode 100644 index 0000000..a9eec4c --- /dev/null +++ b/Runtime/XTable/Rendering/Elements/StyleBoundElement.cs @@ -0,0 +1,170 @@ +using TMPro; + +using UnityEngine; +using UnityEngine.UI; + +using XericLibrary.Runtime.SuperStyleSheet; + +namespace XericUI.XTable.Rendering.Elements +{ + /// + /// 样式绑定元素——将 Image 或 TMP_Text 挂勾到样式表路径。 + /// 自动从 StyleManager 拉取值并应用,样式变更时自动刷新。 + /// 通过对象池复用时调用 Bind/Unbind 切换绑定目标。 + /// + public class StyleBoundElement + { + #region 字段 + + /// 绑定的 Image 组件(可能为 null) + public Image ImageComponent; + + /// 绑定的 TMP_Text 组件(可能为 null) + public TMP_Text TextComponent; + + /// 样式命名空间 + private string m_StyleNamespace; + + /// 各路径的 StyleField 缓存 + private System.Collections.Generic.Dictionary m_Fields + = new System.Collections.Generic.Dictionary(); + + /// 是否已绑定 + private bool m_IsBound; + + #endregion + + #region 公开方法 + + /// + /// 绑定到指定命名空间并开始监听样式。 + /// + public void Bind(string styleNamespace) + { + if (m_IsBound) Unbind(); + m_StyleNamespace = styleNamespace; + + if (string.IsNullOrEmpty(styleNamespace)) return; + + // 预注册常用路径 + RegisterField("/cell/bg/color", OnStyleChanged); + RegisterField("/cell/fg/color", OnStyleChanged); + RegisterField("/cell/border/top/color", OnStyleChanged); + RegisterField("/cell/border/bottom/color", OnStyleChanged); + RegisterField("/cell/border/left/color", OnStyleChanged); + RegisterField("/cell/border/right/color", OnStyleChanged); + RegisterField("/cell/font/size", OnStyleChanged); + RegisterField("/cell/font/alignment", OnStyleChanged); + RegisterField("/cell/font/richText", OnStyleChanged); + + m_IsBound = true; + RefreshAll(); + } + + /// + /// 解绑并停止监听样式。 + /// + public void Unbind() + { + if (!m_IsBound) return; + + foreach (var kvp in m_Fields) + { + if (kvp.Value != null) + { + kvp.Value.OnValueChanged -= OnStyleChanged; + kvp.Value.Unregister(); + } + } + m_Fields.Clear(); + m_IsBound = false; + } + + /// + /// 刷新全部样式值到组件。 + /// + public void RefreshAll() + { + if (!m_IsBound || string.IsNullOrEmpty(m_StyleNamespace)) return; + + // 背景颜色 → Image.color + StyleValue bgVal = GetStyleValue("/cell/bg/color"); + if (bgVal != null && ImageComponent != null) + ImageComponent.color = bgVal; + + // 前景颜色 → Text.color + StyleValue fgVal = GetStyleValue("/cell/fg/color"); + if (fgVal != null && TextComponent != null) + TextComponent.color = fgVal; + + // 字体大小 + StyleValue sizeVal = GetStyleValue("/cell/font/size"); + if (sizeVal != null && TextComponent != null) + TextComponent.fontSize = (int)sizeVal; + + // 对齐 + StyleValue alignVal = GetStyleValue("/cell/font/alignment"); + if (alignVal != null && TextComponent != null) + TextComponent.alignment = alignVal.Convert(); + + // 富文本 + StyleValue richVal = GetStyleValue("/cell/font/richText"); + if (richVal != null && TextComponent != null) + TextComponent.richText = (bool)richVal; + } + + #endregion + + #region 私有方法 + + private void RegisterField(string path, System.Action callback) + { + var field = new StyleField + { + Namespace = m_StyleNamespace, + StylePath = path + }; + field.Register(); + field.OnValueChanged += callback; + m_Fields[path] = field; + } + + private StyleValue GetStyleValue(string path) + { + return StyleManager.GetValue(m_StyleNamespace, path); + } + + private void OnStyleChanged(StyleValue val) + { + RefreshAll(); + } + + #endregion + } + + /// + /// 用于 TextAnchor → TextAlignmentOptions 的转换扩展。 + /// + internal static class StyleBoundElementExtensions + { + public static TextAlignmentOptions Convert(this StyleValue val) + { + string s = val?.GetValueAs(); + if (string.IsNullOrEmpty(s)) return TextAlignmentOptions.Midline; + + switch (s) + { + case "UpperLeft": return TextAlignmentOptions.TopLeft; + case "UpperCenter": return TextAlignmentOptions.Top; + case "UpperRight": return TextAlignmentOptions.TopRight; + case "MiddleLeft": return TextAlignmentOptions.MidlineLeft; + case "MiddleCenter": return TextAlignmentOptions.Midline; + case "MiddleRight": return TextAlignmentOptions.MidlineRight; + case "LowerLeft": return TextAlignmentOptions.BottomLeft; + case "LowerCenter": return TextAlignmentOptions.Bottom; + case "LowerRight": return TextAlignmentOptions.BottomRight; + default: return TextAlignmentOptions.Midline; + } + } + } +} diff --git a/Runtime/XTable/Rendering/Elements/StyleBoundElement.cs.meta b/Runtime/XTable/Rendering/Elements/StyleBoundElement.cs.meta new file mode 100644 index 0000000..dab3add --- /dev/null +++ b/Runtime/XTable/Rendering/Elements/StyleBoundElement.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: bd19dc2a8b3f43348a76e2d1cb8e228e +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/XTable/Rendering/UIToolkit.meta b/Runtime/XTable/Rendering/UIToolkit.meta new file mode 100644 index 0000000..4b82df9 --- /dev/null +++ b/Runtime/XTable/Rendering/UIToolkit.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 7367518f7495815439e1a32ee820f1c5 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/XTable/Rendering/UIToolkit/XTableUITKRenderer.cs b/Runtime/XTable/Rendering/UIToolkit/XTableUITKRenderer.cs new file mode 100644 index 0000000..439914f --- /dev/null +++ b/Runtime/XTable/Rendering/UIToolkit/XTableUITKRenderer.cs @@ -0,0 +1,16 @@ +using UnityEngine; + +namespace XericUI.XTable.Rendering.UIToolkit +{ + /// + /// UI Toolkit 渲染器(预留)——后续实现表格在 UI Toolkit 上的渲染。 + /// 使用 Unity 原生 CSS 样式表。 + /// + public class XTableUITKRenderer + { + // TODO: 实现 UI Toolkit 渲染逻辑 + // - 使用 VisualElement 构建表格 + // - 通过 USS (Unity Style Sheet) 控制样式 + // - 实现虚拟化滚动 + } +} diff --git a/Runtime/XTable/Rendering/UIToolkit/XTableUITKRenderer.cs.meta b/Runtime/XTable/Rendering/UIToolkit/XTableUITKRenderer.cs.meta new file mode 100644 index 0000000..99b7ed1 --- /dev/null +++ b/Runtime/XTable/Rendering/UIToolkit/XTableUITKRenderer.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 5b2e42673c88a2e40853175ade86ec7b +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/XTable/Rendering/XTableViewRect.cs b/Runtime/XTable/Rendering/XTableViewRect.cs new file mode 100644 index 0000000..03e24c8 --- /dev/null +++ b/Runtime/XTable/Rendering/XTableViewRect.cs @@ -0,0 +1,107 @@ +using UnityEngine; + +namespace XericUI.XTable.Rendering +{ + /// + /// 左上角坐标系的视图矩形。 + /// Unity 的 Rect 是左下角原点(Y-up),表格数据索引是左上角原点(Y-down)。 + /// 此类负责坐标转换和可见单元格范围计算。 + /// + public struct XTableViewRect + { + /// 左上角 X 坐标 + public float X; + + /// 左上角 Y 坐标(从上到下递增) + public float Y; + + /// 视图宽度 + public float Width; + + /// 视图高度 + public float Height; + + /// 右边界 + public float XMax => X + Width; + + /// 下边界 + public float YMax => Y + Height; + + /// + /// 从 Unity Rect(左下角原点)转换为 TableViewRect(左上角原点) + /// + /// Unity 坐标系下的矩形 + /// 容器总高度(用于 Y 轴翻转) + public static XTableViewRect FromUnityRect(Rect unityRect, float containerHeight) + { + return new XTableViewRect + { + X = unityRect.xMin, + Y = containerHeight - unityRect.yMax, + Width = unityRect.width, + Height = unityRect.height + }; + } + + /// + /// 转换为 Unity Rect(左下角原点) + /// + public Rect ToUnityRect(float containerHeight) + { + return new Rect(X, containerHeight - Y - Height, Width, Height); + } + + /// + /// 通过行列尺寸计算可见单元格范围。 + /// 累加行高/列宽直到超出视图范围。 + /// + /// 每行高度数组 + /// 每列宽度数组 + /// 可见起始行(包含) + /// 可见结束行(不包含) + /// 可见起始列(包含) + /// 可见结束列(不包含) + public void GetVisibleCellRange( + float[] rowHeights, float[] colWidths, + out int startRow, out int endRow, + out int startCol, out int endCol) + { + startRow = 0; + endRow = 0; + startCol = 0; + endCol = 0; + + if (rowHeights == null || colWidths == null) return; + + // 计算行范围(Y 方向,从上到下) + float accumulatedY = 0f; + for (int r = 0; r < rowHeights.Length; r++) + { + float rowBottom = accumulatedY + rowHeights[r]; + if (rowBottom > Y && accumulatedY < YMax) + { + if (startRow == 0 && accumulatedY < YMax) + startRow = r; + endRow = r + 1; + } + accumulatedY = rowBottom; + if (accumulatedY > YMax) break; + } + + // 计算列范围(X 方向,从左到右) + float accumulatedX = 0f; + for (int c = 0; c < colWidths.Length; c++) + { + float colRight = accumulatedX + colWidths[c]; + if (colRight > X && accumulatedX < XMax) + { + if (startCol == 0 && accumulatedX < XMax) + startCol = c; + endCol = c + 1; + } + accumulatedX = colRight; + if (accumulatedX > XMax) break; + } + } + } +} diff --git a/Runtime/XTable/Rendering/XTableViewRect.cs.meta b/Runtime/XTable/Rendering/XTableViewRect.cs.meta new file mode 100644 index 0000000..d72a125 --- /dev/null +++ b/Runtime/XTable/Rendering/XTableViewRect.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: f64af1d0c0781de499619595c05d0061 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/XTable/Style.meta b/Runtime/XTable/Style.meta new file mode 100644 index 0000000..a2485a1 --- /dev/null +++ b/Runtime/XTable/Style.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 64d88208839a5034db31c330796453aa +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/XTable/Style/DefaultTableStyles.xsss b/Runtime/XTable/Style/DefaultTableStyles.xsss new file mode 100644 index 0000000..466e710 --- /dev/null +++ b/Runtime/XTable/Style/DefaultTableStyles.xsss @@ -0,0 +1,38 @@ +// ========================================== +// Xeric UI Action Table - Default Style Sheet +// 表格默认样式定义 +// ========================================== + +namespace: xeric_table_default + +// ---- 单元格背景 ---- +/cell/bg/color = (1,1,1,1):UnityEngine.Color @Desc:单元格背景颜色 + +// ---- 单元格前景 ---- +/cell/fg/color = (0.1,0.1,0.1,1):UnityEngine.Color @Desc:单元格前景文字颜色 + +// ---- 边框 ---- +/cell/border/top/color = (0.8,0.8,0.8,1):UnityEngine.Color @Desc:上边框颜色 +/cell/border/top/width = 1:System.Single @Desc:上边框宽度 +/cell/border/bottom/color = (0.8,0.8,0.8,1):UnityEngine.Color @Desc:下边框颜色 +/cell/border/bottom/width = 1:System.Single @Desc:下边框宽度 +/cell/border/left/color = (0.8,0.8,0.8,1):UnityEngine.Color @Desc:左边框颜色 +/cell/border/left/width = 1:System.Single @Desc:左边框宽度 +/cell/border/right/color = (0.8,0.8,0.8,1):UnityEngine.Color @Desc:右边框颜色 +/cell/border/right/width = 1:System.Single @Desc:右边框宽度 + +// ---- 选中高亮 ---- +/cell/selection/bg/color = (0.2,0.5,1,0.3):UnityEngine.Color @Desc:选中单元格高亮颜色 + +// ---- 表头样式 ---- +/header/bg/color = (0.9,0.9,0.9,1):UnityEngine.Color @Desc:表头背景颜色 +/header/fg/color = (0,0,0,1):UnityEngine.Color @Desc:表头文字颜色 + +// ---- 字体 ---- +/cell/font/size = 14:System.Int32 @Desc:默认字体大小 +/cell/font/alignment = MiddleCenter:UnityEngine.TextAnchor @Desc:默认文本对齐 +/cell/font/richText = true:System.Boolean @Desc:是否启用富文本 + +// ---- 行高 / 列宽(缺省值) ---- +/cell/default/height = 40:System.Single @Desc:默认行高 +/cell/default/width = 100:System.Single @Desc:默认列宽 diff --git a/Runtime/XTable/Style/DefaultTableStyles.xsss.meta b/Runtime/XTable/Style/DefaultTableStyles.xsss.meta new file mode 100644 index 0000000..4467cef --- /dev/null +++ b/Runtime/XTable/Style/DefaultTableStyles.xsss.meta @@ -0,0 +1,10 @@ +fileFormatVersion: 2 +guid: a2465cbc9f8bf384aa95561b40bd4841 +ScriptedImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 2 + userData: + assetBundleName: + assetBundleVariant: + script: {fileID: 1693830282, guid: 65aae7f37e34be6409e0966d54c89b6d, type: 3}