新Head
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ea823e8c44a0ca842a3b0e59390994de
|
||||
guid: ef107ab630341ed4083145b5a21db705
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
163
Assets/Scripts/DynamicUI/Core/DrawerRegistry.cs
Normal file
163
Assets/Scripts/DynamicUI/Core/DrawerRegistry.cs
Normal file
@@ -0,0 +1,163 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Ichni.RhythmGame;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Ichni.Editor
|
||||
{
|
||||
/// <summary>
|
||||
/// 类型→绘制器注册表。集中管理 Prefab 引用和 Type→IPropertyDrawer 映射。
|
||||
/// 替代分散在 IHaveInspection 各 Generate 方法中的 basePrefabs 直接访问。
|
||||
/// 使用惰性单例,首次访问时自动注册所有内置 Drawer。
|
||||
/// </summary>
|
||||
public class DrawerRegistry
|
||||
{
|
||||
private static DrawerRegistry _instance;
|
||||
|
||||
/// <summary> 全局单例 </summary>
|
||||
public static DrawerRegistry Instance
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_instance == null)
|
||||
{
|
||||
_instance = new DrawerRegistry();
|
||||
_instance.RegisterBuiltInDrawers();
|
||||
}
|
||||
return _instance;
|
||||
}
|
||||
}
|
||||
|
||||
// 按属性类型注册的 Drawer (优先级最高)
|
||||
private readonly Dictionary<Type, IPropertyDrawer> _attributeDrawers = new Dictionary<Type, IPropertyDrawer>();
|
||||
|
||||
// 按字段类型注册的 Drawer
|
||||
private readonly Dictionary<Type, IPropertyDrawer> _typeDrawers = new Dictionary<Type, IPropertyDrawer>();
|
||||
|
||||
// 特殊 Drawer (Button, HintText 等不绑定字段类型的)
|
||||
private readonly Dictionary<string, IPropertyDrawer> _namedDrawers = new Dictionary<string, IPropertyDrawer>();
|
||||
|
||||
// Fallback Drawer
|
||||
private IPropertyDrawer _fallbackDrawer;
|
||||
|
||||
/// <summary> Prefab 引用桥接到 BasePrefabsCollection </summary>
|
||||
private BasePrefabsCollection Prefabs => EditorManager.instance.basePrefabs;
|
||||
|
||||
/// <summary> 注册字段类型→Drawer 映射 </summary>
|
||||
public void RegisterDrawer(Type fieldType, IPropertyDrawer drawer)
|
||||
{
|
||||
_typeDrawers[fieldType] = drawer;
|
||||
}
|
||||
|
||||
/// <summary> 注册属性类型→Drawer 映射(属性特化优先于字段类型) </summary>
|
||||
public void RegisterAttributeDrawer<TAttr>(IPropertyDrawer drawer) where TAttr : Attribute
|
||||
{
|
||||
_attributeDrawers[typeof(TAttr)] = drawer;
|
||||
}
|
||||
|
||||
/// <summary> 注册命名 Drawer(用于 Button、HintText 等非类型绑定的控件) </summary>
|
||||
public void RegisterNamedDrawer(string name, IPropertyDrawer drawer)
|
||||
{
|
||||
_namedDrawers[name] = drawer;
|
||||
}
|
||||
|
||||
/// <summary> 设置 Fallback Drawer(类型未注册时使用) </summary>
|
||||
public void SetFallbackDrawer(IPropertyDrawer drawer)
|
||||
{
|
||||
_fallbackDrawer = drawer;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取适用的 Drawer。查找优先级:
|
||||
/// 1. 属性注册(如 [InspectorSlider] → SliderDrawer)
|
||||
/// 2. 类型注册(如 bool → ToggleDrawer)
|
||||
/// 3. Enum 基类型检测
|
||||
/// 4. Fallback (InputFieldDrawer)
|
||||
/// </summary>
|
||||
public IPropertyDrawer GetDrawer(Type fieldType, Attribute[] attributes = null)
|
||||
{
|
||||
// 1. 属性特化优先
|
||||
if (attributes != null)
|
||||
{
|
||||
foreach (var attr in attributes)
|
||||
{
|
||||
if (_attributeDrawers.TryGetValue(attr.GetType(), out var attrDrawer))
|
||||
return attrDrawer;
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 精确类型匹配
|
||||
if (fieldType != null && _typeDrawers.TryGetValue(fieldType, out var typeDrawer))
|
||||
return typeDrawer;
|
||||
|
||||
// 3. Enum 基类型检测
|
||||
if (fieldType != null && fieldType.IsEnum && _typeDrawers.TryGetValue(typeof(Enum), out var enumDrawer))
|
||||
return enumDrawer;
|
||||
|
||||
// 4. Fallback
|
||||
return _fallbackDrawer;
|
||||
}
|
||||
|
||||
/// <summary> 获取命名 Drawer </summary>
|
||||
public IPropertyDrawer GetNamedDrawer(string name)
|
||||
{
|
||||
_namedDrawers.TryGetValue(name, out var drawer);
|
||||
return drawer;
|
||||
}
|
||||
|
||||
/// <summary> 获取 DynamicUI Element Prefab </summary>
|
||||
public GameObject GetPrefab(string elementType)
|
||||
{
|
||||
return elementType switch
|
||||
{
|
||||
"button" => Prefabs.button,
|
||||
"toggle" => Prefabs.toggle,
|
||||
"inputField" => Prefabs.inputField,
|
||||
"slider" => Prefabs.slider,
|
||||
"vector2InputField" => Prefabs.vector2InputField,
|
||||
"vector3InputField" => Prefabs.vector3InputField,
|
||||
"enumDropdown" => Prefabs.enumDropdown,
|
||||
"stringListDropdown" => Prefabs.stringListDropdown,
|
||||
"baseColorPicker" => Prefabs.baseColorPicker,
|
||||
"emissionColorPicker" => Prefabs.emissionColorPicker,
|
||||
"hintText" => Prefabs.hintText,
|
||||
"parameterText" => Prefabs.parameterText,
|
||||
"hsvDrawer" => Prefabs.hsvDrawer,
|
||||
"container" => Prefabs.dynamicUIContainer,
|
||||
"subcontainer" => Prefabs.dynamicUISubcontainer,
|
||||
_ => null,
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 注册所有内置 Drawer,覆盖全部基本字段类型和特化属性。
|
||||
/// </summary>
|
||||
private void RegisterBuiltInDrawers()
|
||||
{
|
||||
// 基本类型 Drawer
|
||||
var inputFieldDrawer = new InputFieldDrawer();
|
||||
RegisterDrawer(typeof(float), inputFieldDrawer);
|
||||
RegisterDrawer(typeof(int), inputFieldDrawer);
|
||||
RegisterDrawer(typeof(string), inputFieldDrawer);
|
||||
SetFallbackDrawer(inputFieldDrawer);
|
||||
|
||||
RegisterDrawer(typeof(bool), new ToggleDrawer());
|
||||
RegisterDrawer(typeof(System.Enum), new EnumDropdownDrawer());
|
||||
RegisterDrawer(typeof(Vector2), new Vector2FieldDrawer());
|
||||
RegisterDrawer(typeof(Vector3), new Vector3FieldDrawer());
|
||||
RegisterDrawer(typeof(Color), new BaseColorPickerDrawer());
|
||||
|
||||
// 属性特化 Drawer
|
||||
RegisterAttributeDrawer<InspectorSliderAttribute>(new SliderDrawer());
|
||||
RegisterAttributeDrawer<InspectorEmissionColorAttribute>(new EmissionColorPickerDrawer());
|
||||
|
||||
// 命名 Drawer(非类型绑定的控件)
|
||||
RegisterNamedDrawer("button", new ButtonDrawer());
|
||||
RegisterNamedDrawer("hintText", new HintTextDrawer());
|
||||
RegisterNamedDrawer("parameterText", new ParameterTextDrawer());
|
||||
RegisterNamedDrawer("slider", new SliderDrawer());
|
||||
RegisterNamedDrawer("stringListDropdown", new StringListDropdownDrawer());
|
||||
RegisterNamedDrawer("emissionColorPicker", new EmissionColorPickerDrawer());
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/DynamicUI/Core/DrawerRegistry.cs.meta
Normal file
2
Assets/Scripts/DynamicUI/Core/DrawerRegistry.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b796d9d7294bb5248a040f9e7dc8be8b
|
||||
27
Assets/Scripts/DynamicUI/Core/ElementRef.cs
Normal file
27
Assets/Scripts/DynamicUI/Core/ElementRef.cs
Normal file
@@ -0,0 +1,27 @@
|
||||
namespace Ichni.Editor
|
||||
{
|
||||
/// <summary>
|
||||
/// 延迟引用容器 —— 在 Build() 时由 InspectorBuilder 填充。
|
||||
/// 用于在 Button 回调中访问其他控件(如 UnboundInputField 的值)。
|
||||
///
|
||||
/// 用法:
|
||||
/// <code>
|
||||
/// var nameRef = new ElementRef<DynamicUIInputField>();
|
||||
/// InspectorBuilder.For(this)
|
||||
/// .Section("Search")
|
||||
/// .UnboundInputField("Name").WithRef(nameRef)
|
||||
/// .Button("Find", () => {
|
||||
/// string name = nameRef.Value?.GetValue<string>();
|
||||
/// })
|
||||
/// .Build();
|
||||
/// </code>
|
||||
/// </summary>
|
||||
public class ElementRef<T> where T : DynamicUIElement
|
||||
{
|
||||
/// <summary> 构建后填充的控件实例,Build() 之前为 null </summary>
|
||||
public T Value { get; internal set; }
|
||||
|
||||
/// <summary> 隐式转换,方便直接当 T 使用 </summary>
|
||||
public static implicit operator T(ElementRef<T> r) => r?.Value;
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/DynamicUI/Core/ElementRef.cs.meta
Normal file
2
Assets/Scripts/DynamicUI/Core/ElementRef.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 2de2ed51f60dceb469874524fb69bd28
|
||||
118
Assets/Scripts/DynamicUI/Core/IPropertyDrawer.cs
Normal file
118
Assets/Scripts/DynamicUI/Core/IPropertyDrawer.cs
Normal file
@@ -0,0 +1,118 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Ichni.RhythmGame;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Ichni.Editor
|
||||
{
|
||||
/// <summary>
|
||||
/// 可插拔属性绘制器接口。每种字段类型(bool, float, Color 等)
|
||||
/// 对应一个 Drawer 实现,负责创建和配置对应类型的 DynamicUIElement。
|
||||
/// </summary>
|
||||
public interface IPropertyDrawer
|
||||
{
|
||||
/// <summary> 默认占据的列数 (1-3),1 = 1/3 行宽 </summary>
|
||||
int DefaultSpan { get; }
|
||||
|
||||
/// <summary> 默认高度 (px) </summary>
|
||||
float DefaultHeight { get; }
|
||||
|
||||
/// <summary> 创建并初始化一个 DynamicUIElement,挂载到 parent 下 </summary>
|
||||
DynamicUIElement Draw(DrawContext ctx);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 绘制上下文,传递给 IPropertyDrawer.Draw() 的所有必要信息。
|
||||
/// 由 InspectorBuilder 在 Build() 时构造。
|
||||
/// </summary>
|
||||
public struct DrawContext
|
||||
{
|
||||
/// <summary> 绑定的目标元素 </summary>
|
||||
public IBaseElement Target;
|
||||
|
||||
/// <summary> 反射路径(ReflectionHelper 使用的字段名) </summary>
|
||||
public string FieldName;
|
||||
|
||||
/// <summary> 显示标签 </summary>
|
||||
public string Label;
|
||||
|
||||
/// <summary> 父容器(Row 的 RectTransform) </summary>
|
||||
public Transform Parent;
|
||||
|
||||
/// <summary> Drawer 注册表,用于获取 Prefab 引用 </summary>
|
||||
public DrawerRegistry Registry;
|
||||
|
||||
/// <summary> 目标 Inspector 窗口(用于 Mark 和 MarkedElements 注册) </summary>
|
||||
public IHaveInspection Inspection;
|
||||
|
||||
// ──────── 可选配置(由 Builder 链式方法设置)────────
|
||||
|
||||
/// <summary> 覆盖默认 Span </summary>
|
||||
public int? OverrideSpan;
|
||||
|
||||
/// <summary> 覆盖默认高度 </summary>
|
||||
public float? OverrideHeight;
|
||||
|
||||
/// <summary> 是否启用自动更新 </summary>
|
||||
public bool AutoUpdate;
|
||||
|
||||
/// <summary> 是否只读 </summary>
|
||||
public bool ReadOnly;
|
||||
|
||||
/// <summary> 值变更时的回调 </summary>
|
||||
public Action OnChangedCallback;
|
||||
|
||||
/// <summary> 控件启用条件(返回 false 时禁用交互) </summary>
|
||||
public Func<bool> EnabledCondition;
|
||||
|
||||
/// <summary> Mark 标记键 </summary>
|
||||
public string MarkKey;
|
||||
|
||||
// ──────── Slider 专用 ────────
|
||||
public float SliderMin;
|
||||
public float SliderMax;
|
||||
public bool SliderWholeNumbers;
|
||||
|
||||
// ──────── EmissionColor 专用 ────────
|
||||
public string EmissionEnabledName;
|
||||
public string EmissionIntensityName;
|
||||
|
||||
// ──────── Button 专用 ────────
|
||||
public Action ButtonAction;
|
||||
|
||||
// ──────── HintText 专用 ────────
|
||||
public string StaticText;
|
||||
public Func<string> DynamicText;
|
||||
|
||||
// ──────── Dropdown 专用 ────────
|
||||
public Type EnumType;
|
||||
public List<string> StringListOptions;
|
||||
|
||||
// ──────── Unbound 控件 ────────
|
||||
|
||||
/// <summary> 无绑定控件类型 (None = 正常绑定控件) </summary>
|
||||
public UnboundKind Unbound;
|
||||
|
||||
/// <summary> 无绑定 InputField 的默认文本 </summary>
|
||||
public string DefaultText;
|
||||
|
||||
/// <summary> 无绑定 Slider 的默认值 </summary>
|
||||
public float DefaultFloat;
|
||||
|
||||
/// <summary> 无绑定 Vector3Field 的默认值 </summary>
|
||||
public Vector3 DefaultVector3;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 无绑定控件类型枚举。
|
||||
/// None 表示正常的数据绑定控件;其他值表示不绑定任何字段的独立控件。
|
||||
/// </summary>
|
||||
public enum UnboundKind
|
||||
{
|
||||
None = 0,
|
||||
InputField,
|
||||
Toggle,
|
||||
Slider,
|
||||
Vector3Field,
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/DynamicUI/Core/IPropertyDrawer.cs.meta
Normal file
2
Assets/Scripts/DynamicUI/Core/IPropertyDrawer.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: cefee972a8f49184aaff2f2559b67fcf
|
||||
147
Assets/Scripts/DynamicUI/Core/InspectorAttributes.cs
Normal file
147
Assets/Scripts/DynamicUI/Core/InspectorAttributes.cs
Normal file
@@ -0,0 +1,147 @@
|
||||
using System;
|
||||
|
||||
namespace Ichni.Editor
|
||||
{
|
||||
/// <summary>
|
||||
/// 字段/属性标注 —— 驱动 InspectorBuilder.AutoBuild() 自动生成 UI 控件。
|
||||
/// 替代旧版 DynamicUIAttribute,新增 GroupOrder 和 Order 排序支持。
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property)]
|
||||
public class InspectorFieldAttribute : Attribute
|
||||
{
|
||||
/// <summary> 显示名称,为空时使用成员名 </summary>
|
||||
public string Name { get; set; }
|
||||
|
||||
/// <summary> 所属 Section 标题 </summary>
|
||||
public string Group { get; set; }
|
||||
|
||||
/// <summary> Section 排序权重(与 InspectorBuilder.Section 的 sectionOrder 统一排序,升序) </summary>
|
||||
public int GroupOrder { get; set; }
|
||||
|
||||
/// <summary> Section 内元素排序权重(升序,同 order 按声明顺序 stable sort) </summary>
|
||||
public int Order { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 排版格子要求。一行最大宽度分为 3 份。
|
||||
/// 填 1 代表需占 1/3。填 3 代表全宽占满整行。
|
||||
/// 填 0 由内置工厂根据数据类型自动识别。
|
||||
/// </summary>
|
||||
public int Span { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 垂直占位高度 (px)。填 0 由系统按类型智能推断。
|
||||
/// </summary>
|
||||
public float Height { get; set; }
|
||||
|
||||
/// <summary> 是否自动更新(绑定 IHaveAutoUpdate) </summary>
|
||||
public bool AutoUpdate { get; set; }
|
||||
|
||||
/// <summary> 只读模式(生成 HintText 替代可编辑控件) </summary>
|
||||
public bool ReadOnly { get; set; }
|
||||
|
||||
public InspectorFieldAttribute(
|
||||
string name = "",
|
||||
string group = "Default",
|
||||
int groupOrder = 0,
|
||||
int order = 0,
|
||||
int span = 0,
|
||||
float height = 0f,
|
||||
bool autoUpdate = false,
|
||||
bool readOnly = false)
|
||||
{
|
||||
Name = name;
|
||||
Group = group;
|
||||
GroupOrder = groupOrder;
|
||||
Order = order;
|
||||
Span = span;
|
||||
Height = height;
|
||||
AutoUpdate = autoUpdate;
|
||||
ReadOnly = readOnly;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Slider 控件标注,适用于 float/int 类型字段。
|
||||
/// 替代旧版 DynamicUISliderAttribute。
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property)]
|
||||
public class InspectorSliderAttribute : InspectorFieldAttribute
|
||||
{
|
||||
public float Min { get; set; }
|
||||
public float Max { get; set; }
|
||||
public bool WholeNumbers { get; set; }
|
||||
|
||||
public InspectorSliderAttribute(
|
||||
string name = "",
|
||||
string group = "Default",
|
||||
float min = 0f,
|
||||
float max = 1f,
|
||||
bool wholeNumbers = false,
|
||||
int groupOrder = 0,
|
||||
int order = 0,
|
||||
int span = 0,
|
||||
float height = 0f,
|
||||
bool autoUpdate = false,
|
||||
bool readOnly = false)
|
||||
: base(name, group, groupOrder, order, span, height, autoUpdate, readOnly)
|
||||
{
|
||||
Min = min;
|
||||
Max = max;
|
||||
WholeNumbers = wholeNumbers;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// EmissionColor 拾色器标注,支持 HDR 及独立 Emission 强度/开关通道。
|
||||
/// 替代旧版 DynamicUIEmissionColorAttribute。
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property)]
|
||||
public class InspectorEmissionColorAttribute : InspectorFieldAttribute
|
||||
{
|
||||
/// <summary> Emission 开关字段名,"NULL" 表示强制启用 </summary>
|
||||
public string EmissionEnabledName { get; set; }
|
||||
|
||||
/// <summary> Emission 强度字段名,"NULL" 表示使用颜色 Alpha 通道 </summary>
|
||||
public string EmissionIntensityName { get; set; }
|
||||
|
||||
public InspectorEmissionColorAttribute(
|
||||
string name,
|
||||
string enabledName = "NULL",
|
||||
string intensityName = "NULL",
|
||||
string group = "Default",
|
||||
int groupOrder = 0,
|
||||
int order = 0,
|
||||
int span = 3,
|
||||
float height = 0f)
|
||||
: base(name, group, groupOrder, order, span, height)
|
||||
{
|
||||
EmissionEnabledName = enabledName;
|
||||
EmissionIntensityName = intensityName;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 按钮标注,挂载在无参 Method 上。
|
||||
/// 替代旧版 DynamicUIButtonAttribute。
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Method)]
|
||||
public class InspectorButtonAttribute : Attribute
|
||||
{
|
||||
public string Name { get; set; }
|
||||
public string Group { get; set; }
|
||||
public int GroupOrder { get; set; }
|
||||
public int Order { get; set; }
|
||||
|
||||
public InspectorButtonAttribute(
|
||||
string name = "",
|
||||
string group = "Default",
|
||||
int groupOrder = 0,
|
||||
int order = 0)
|
||||
{
|
||||
Name = name;
|
||||
Group = group;
|
||||
GroupOrder = groupOrder;
|
||||
Order = order;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: be7c441b9cc629e4287d879fd13a700f
|
||||
1203
Assets/Scripts/DynamicUI/Core/InspectorBuilder.cs
Normal file
1203
Assets/Scripts/DynamicUI/Core/InspectorBuilder.cs
Normal file
File diff suppressed because it is too large
Load Diff
2
Assets/Scripts/DynamicUI/Core/InspectorBuilder.cs.meta
Normal file
2
Assets/Scripts/DynamicUI/Core/InspectorBuilder.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f497f16c91f01db48ab2e21dc67a0f3b
|
||||
181
Assets/Scripts/DynamicUI/Core/LayoutPacker.cs
Normal file
181
Assets/Scripts/DynamicUI/Core/LayoutPacker.cs
Normal file
@@ -0,0 +1,181 @@
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Ichni.Editor
|
||||
{
|
||||
/// <summary>
|
||||
/// 自动行打包算法 —— 将元素列表按 Span 和高度层级自动装入行。
|
||||
/// 替代手动调用 GenerateSubcontainer(N) 手动分配列数。
|
||||
/// </summary>
|
||||
public static class LayoutPacker
|
||||
{
|
||||
/// <summary> 每行最大 Span 总量 </summary>
|
||||
public const int MaxSpanPerRow = 3;
|
||||
|
||||
/// <summary> 行总宽度 (px),与 DynamicUI 现有 600px 布局一致 </summary>
|
||||
public const float TotalRowWidth = 600f;
|
||||
|
||||
/// <summary> 默认行高 (px) </summary>
|
||||
public const float DefaultRowHeight = 100f;
|
||||
|
||||
/// <summary>
|
||||
/// 高度层级。同一行内只允许相同层级的元素,
|
||||
/// 避免 100px 的 Toggle 和 280px 的 ColorPicker 混排导致空白浪费。
|
||||
/// </summary>
|
||||
public enum HeightTier
|
||||
{
|
||||
/// <summary> 标准高度 (<= 100px): bool, float, int, string, enum, Vector2, Vector3, Button... </summary>
|
||||
Standard,
|
||||
/// <summary> 中等高度 (<= 280px): BaseColorPicker </summary>
|
||||
Tall,
|
||||
/// <summary> 超高 (> 280px): EmissionColorPicker 等 </summary>
|
||||
ExtraTall
|
||||
}
|
||||
|
||||
/// <summary> 待打包的元素定义 </summary>
|
||||
public struct ElementDef
|
||||
{
|
||||
/// <summary> 占据的列数 (1-3) </summary>
|
||||
public int Span;
|
||||
|
||||
/// <summary> 元素高度 (px) </summary>
|
||||
public float Height;
|
||||
|
||||
/// <summary> 高度层级(由 Height 自动推断) </summary>
|
||||
public HeightTier Tier;
|
||||
|
||||
/// <summary> 排序权重(Section 内排序) </summary>
|
||||
public int Order;
|
||||
|
||||
/// <summary> 稳定排序用的原始插入索引 </summary>
|
||||
public int InsertionIndex;
|
||||
|
||||
/// <summary> 绘制上下文 </summary>
|
||||
public DrawContext Context;
|
||||
|
||||
/// <summary> 对应的绘制器 </summary>
|
||||
public IPropertyDrawer Drawer;
|
||||
}
|
||||
|
||||
/// <summary> 一行的定义 </summary>
|
||||
public struct RowDef
|
||||
{
|
||||
/// <summary> 行内元素 </summary>
|
||||
public List<ElementDef> Elements;
|
||||
|
||||
/// <summary> 行高 = max(行内所有元素的 Height) </summary>
|
||||
public float RowHeight;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将元素列表打包为行列表。
|
||||
/// 约束: 同一行内 Span 总和 <= MaxSpanPerRow,且元素必须属于相同 HeightTier。
|
||||
/// 元素先按 Order 升序 stable sort,再按顺序贪心装箱。
|
||||
/// </summary>
|
||||
public static List<RowDef> Pack(List<ElementDef> elements)
|
||||
{
|
||||
var rows = new List<RowDef>();
|
||||
if (elements == null || elements.Count == 0)
|
||||
return rows;
|
||||
|
||||
// Stable sort: 先按 Order 升序,同 Order 按 InsertionIndex 升序
|
||||
elements.Sort((a, b) =>
|
||||
{
|
||||
int cmp = a.Order.CompareTo(b.Order);
|
||||
return cmp != 0 ? cmp : a.InsertionIndex.CompareTo(b.InsertionIndex);
|
||||
});
|
||||
|
||||
var currentRow = new List<ElementDef>();
|
||||
int currentSpan = 0;
|
||||
HeightTier currentTier = elements[0].Tier;
|
||||
float currentMaxHeight = 0f;
|
||||
|
||||
int currentRowSpan = -1; // 当前行元素的 Span 值(-1 表示尚未确定)
|
||||
|
||||
for (int i = 0; i < elements.Count; i++)
|
||||
{
|
||||
var elem = elements[i];
|
||||
int elemSpan = Mathf.Clamp(elem.Span, 1, MaxSpanPerRow);
|
||||
|
||||
bool spanOverflow = currentSpan + elemSpan > MaxSpanPerRow;
|
||||
bool tierMismatch = currentRow.Count > 0 && elem.Tier != currentTier;
|
||||
// GridLayoutGroup 强制统一 cellSize,同一行内混搭不同 Span 宽度会导致变形
|
||||
bool spanMismatch = currentRow.Count > 0 && elemSpan != currentRowSpan;
|
||||
|
||||
if (spanOverflow || tierMismatch || spanMismatch)
|
||||
{
|
||||
// 当前行已满、高度层级不兼容或 Span 不一致,提交当前行
|
||||
FlushRow(rows, currentRow, currentMaxHeight);
|
||||
currentRow = new List<ElementDef>();
|
||||
currentSpan = 0;
|
||||
currentMaxHeight = 0f;
|
||||
currentTier = elem.Tier;
|
||||
currentRowSpan = -1;
|
||||
}
|
||||
|
||||
currentRow.Add(elem);
|
||||
currentSpan += elemSpan;
|
||||
currentRowSpan = elemSpan;
|
||||
if (elem.Height > currentMaxHeight)
|
||||
currentMaxHeight = elem.Height;
|
||||
}
|
||||
|
||||
// 提交最后一行
|
||||
if (currentRow.Count > 0)
|
||||
{
|
||||
FlushRow(rows, currentRow, currentMaxHeight);
|
||||
}
|
||||
|
||||
return rows;
|
||||
}
|
||||
|
||||
/// <summary> 根据高度值推断 HeightTier </summary>
|
||||
public static HeightTier InferTier(float height)
|
||||
{
|
||||
if (height <= DefaultRowHeight)
|
||||
return HeightTier.Standard;
|
||||
if (height <= 280f)
|
||||
return HeightTier.Tall;
|
||||
return HeightTier.ExtraTall;
|
||||
}
|
||||
|
||||
/// <summary> 根据类型推断默认 Span </summary>
|
||||
public static int InferSpan(System.Type fieldType)
|
||||
{
|
||||
if (fieldType == null) return 1;
|
||||
if (fieldType == typeof(bool)) return 1;
|
||||
if (fieldType == typeof(float) || fieldType == typeof(int) || fieldType == typeof(string)) return 1;
|
||||
if (fieldType.IsEnum) return 1;
|
||||
if (fieldType == typeof(Vector2)) return 2;
|
||||
if (fieldType == typeof(Vector3) || fieldType == typeof(Color)) return 3;
|
||||
return 1;
|
||||
}
|
||||
|
||||
/// <summary> 根据类型推断默认高度 </summary>
|
||||
public static float InferHeight(System.Type fieldType, System.Attribute[] attributes = null)
|
||||
{
|
||||
if (attributes != null)
|
||||
{
|
||||
foreach (var attr in attributes)
|
||||
{
|
||||
if (attr is InspectorEmissionColorAttribute)
|
||||
return 320f;
|
||||
}
|
||||
}
|
||||
|
||||
if (fieldType == typeof(Color))
|
||||
return 280f;
|
||||
|
||||
return DefaultRowHeight;
|
||||
}
|
||||
|
||||
private static void FlushRow(List<RowDef> rows, List<ElementDef> rowElements, float maxHeight)
|
||||
{
|
||||
rows.Add(new RowDef
|
||||
{
|
||||
Elements = rowElements,
|
||||
RowHeight = Mathf.Max(maxHeight, DefaultRowHeight)
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/DynamicUI/Core/LayoutPacker.cs.meta
Normal file
2
Assets/Scripts/DynamicUI/Core/LayoutPacker.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 4e8a9feff817f194da598f2ef0c357ea
|
||||
8
Assets/Scripts/DynamicUI/Drawers.meta
Normal file
8
Assets/Scripts/DynamicUI/Drawers.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 25964ec060be79b4f835e09d9383ad7a
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
37
Assets/Scripts/DynamicUI/Drawers/BaseColorPickerDrawer.cs
Normal file
37
Assets/Scripts/DynamicUI/Drawers/BaseColorPickerDrawer.cs
Normal file
@@ -0,0 +1,37 @@
|
||||
using Ichni.RhythmGame;
|
||||
using Lean.Pool;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
|
||||
namespace Ichni.Editor
|
||||
{
|
||||
/// <summary>
|
||||
/// 处理 Color 类型的 Drawer。
|
||||
/// 创建 DynamicUIBaseColorPicker。
|
||||
/// </summary>
|
||||
public class BaseColorPickerDrawer : IPropertyDrawer
|
||||
{
|
||||
public int DefaultSpan => 3;
|
||||
public float DefaultHeight => 280f;
|
||||
|
||||
public DynamicUIElement Draw(DrawContext ctx)
|
||||
{
|
||||
var go = LeanPool.Spawn(ctx.Registry.GetPrefab("baseColorPicker"), ctx.Parent);
|
||||
var element = go.GetComponent<DynamicUIBaseColorPicker>();
|
||||
|
||||
element.Initialize(ctx.Target, ctx.Label, ctx.FieldName);
|
||||
|
||||
DynamicUIElement.DisableNavigation(
|
||||
element.inputFieldBaseR,
|
||||
element.inputFieldBaseG,
|
||||
element.inputFieldBaseB,
|
||||
element.inputFieldBaseA);
|
||||
|
||||
int span = ctx.OverrideSpan ?? DefaultSpan;
|
||||
float height = ctx.OverrideHeight ?? DefaultHeight;
|
||||
element.SetLayoutSize(span * LayoutPacker.TotalRowWidth / LayoutPacker.MaxSpanPerRow, height);
|
||||
|
||||
return element;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f148308680f4a4b47a9d1097376ca1aa
|
||||
40
Assets/Scripts/DynamicUI/Drawers/ButtonDrawer.cs
Normal file
40
Assets/Scripts/DynamicUI/Drawers/ButtonDrawer.cs
Normal file
@@ -0,0 +1,40 @@
|
||||
using Lean.Pool;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Events;
|
||||
using UnityEngine.UI;
|
||||
|
||||
namespace Ichni.Editor
|
||||
{
|
||||
/// <summary>
|
||||
/// 按钮 Drawer,不绑定数据字段。
|
||||
/// 创建 DynamicUIButton 并绑定 ButtonAction。
|
||||
/// </summary>
|
||||
public class ButtonDrawer : IPropertyDrawer
|
||||
{
|
||||
public int DefaultSpan => 1;
|
||||
public float DefaultHeight => LayoutPacker.DefaultRowHeight;
|
||||
|
||||
public DynamicUIElement Draw(DrawContext ctx)
|
||||
{
|
||||
var go = LeanPool.Spawn(ctx.Registry.GetPrefab("button"), ctx.Parent);
|
||||
var element = go.GetComponent<DynamicUIButton>();
|
||||
|
||||
element.SetText(ctx.Label);
|
||||
element.Initialize(ctx.Target, ctx.Label, string.Empty);
|
||||
|
||||
if (ctx.ButtonAction != null)
|
||||
{
|
||||
UnityAction action = () => ctx.ButtonAction();
|
||||
element.ApplyFunction(action);
|
||||
}
|
||||
|
||||
DynamicUIElement.DisableNavigation(element.button);
|
||||
|
||||
int span = ctx.OverrideSpan ?? DefaultSpan;
|
||||
float height = ctx.OverrideHeight ?? DefaultHeight;
|
||||
element.SetLayoutSize(span * LayoutPacker.TotalRowWidth / LayoutPacker.MaxSpanPerRow, height);
|
||||
|
||||
return element;
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/DynamicUI/Drawers/ButtonDrawer.cs.meta
Normal file
2
Assets/Scripts/DynamicUI/Drawers/ButtonDrawer.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 23d17d2a4ce34f84a84f140f213d166c
|
||||
@@ -0,0 +1,40 @@
|
||||
using Ichni.RhythmGame;
|
||||
using Lean.Pool;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
|
||||
namespace Ichni.Editor
|
||||
{
|
||||
/// <summary>
|
||||
/// 处理带 [InspectorEmissionColor] 属性的 Color 类型的 Drawer。
|
||||
/// 创建 DynamicUIEmissionColorPicker,支持 Emission 开关和强度控制。
|
||||
/// </summary>
|
||||
public class EmissionColorPickerDrawer : IPropertyDrawer
|
||||
{
|
||||
public int DefaultSpan => 3;
|
||||
public float DefaultHeight => 320f;
|
||||
|
||||
public DynamicUIElement Draw(DrawContext ctx)
|
||||
{
|
||||
var go = LeanPool.Spawn(ctx.Registry.GetPrefab("emissionColorPicker"), ctx.Parent);
|
||||
var element = go.GetComponent<DynamicUIEmissionColorPicker>();
|
||||
|
||||
element.Initialize(ctx.Target, ctx.Label,
|
||||
ctx.EmissionEnabledName ?? "NULL",
|
||||
ctx.FieldName,
|
||||
ctx.EmissionIntensityName ?? "NULL");
|
||||
|
||||
DynamicUIElement.DisableNavigation(
|
||||
element.inputFieldEmissionR,
|
||||
element.inputFieldEmissionG,
|
||||
element.inputFieldEmissionB,
|
||||
element.inputFieldEmissionI);
|
||||
|
||||
int span = ctx.OverrideSpan ?? DefaultSpan;
|
||||
float height = ctx.OverrideHeight ?? DefaultHeight;
|
||||
element.SetLayoutSize(span * LayoutPacker.TotalRowWidth / LayoutPacker.MaxSpanPerRow, height);
|
||||
|
||||
return element;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 444ef9b226757004f9290d1ea11d2c60
|
||||
49
Assets/Scripts/DynamicUI/Drawers/EnumDropdownDrawer.cs
Normal file
49
Assets/Scripts/DynamicUI/Drawers/EnumDropdownDrawer.cs
Normal file
@@ -0,0 +1,49 @@
|
||||
using System;
|
||||
using Ichni.RhythmGame;
|
||||
using Lean.Pool;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Ichni.Editor
|
||||
{
|
||||
/// <summary>
|
||||
/// 处理所有 Enum 类型的 Drawer。
|
||||
/// 通过反射获取字段的枚举类型,创建 DynamicUIEnumDropdown。
|
||||
/// </summary>
|
||||
public class EnumDropdownDrawer : IPropertyDrawer
|
||||
{
|
||||
public int DefaultSpan => 1;
|
||||
public float DefaultHeight => LayoutPacker.DefaultRowHeight;
|
||||
|
||||
public DynamicUIElement Draw(DrawContext ctx)
|
||||
{
|
||||
var go = LeanPool.Spawn(ctx.Registry.GetPrefab("enumDropdown"), ctx.Parent);
|
||||
var element = go.GetComponent<DynamicUIEnumDropdown>();
|
||||
|
||||
// 获取枚举类型:优先使用 DrawContext 中的 EnumType,fallback 到反射
|
||||
Type enumType = ctx.EnumType;
|
||||
if (enumType == null && ctx.Target != null && !string.IsNullOrEmpty(ctx.FieldName))
|
||||
{
|
||||
var field = ctx.Target.GetType().GetField(ctx.FieldName);
|
||||
if (field != null) enumType = field.FieldType;
|
||||
else
|
||||
{
|
||||
var prop = ctx.Target.GetType().GetProperty(ctx.FieldName);
|
||||
if (prop != null) enumType = prop.PropertyType;
|
||||
}
|
||||
}
|
||||
|
||||
if (enumType != null)
|
||||
{
|
||||
element.SetUpEnum(enumType);
|
||||
}
|
||||
|
||||
element.Initialize(ctx.Target, ctx.Label, ctx.FieldName);
|
||||
|
||||
int span = ctx.OverrideSpan ?? DefaultSpan;
|
||||
float height = ctx.OverrideHeight ?? DefaultHeight;
|
||||
element.SetLayoutSize(span * LayoutPacker.TotalRowWidth / LayoutPacker.MaxSpanPerRow, height);
|
||||
|
||||
return element;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 13456a684c2325341a1010f04d0dabb5
|
||||
38
Assets/Scripts/DynamicUI/Drawers/HintTextDrawer.cs
Normal file
38
Assets/Scripts/DynamicUI/Drawers/HintTextDrawer.cs
Normal file
@@ -0,0 +1,38 @@
|
||||
using Lean.Pool;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Ichni.Editor
|
||||
{
|
||||
/// <summary>
|
||||
/// 提示文本 Drawer,支持静态文本和 Func<string> 动态文本。
|
||||
/// 创建 DynamicUIHintText。
|
||||
/// </summary>
|
||||
public class HintTextDrawer : IPropertyDrawer
|
||||
{
|
||||
public int DefaultSpan => 1;
|
||||
public float DefaultHeight => LayoutPacker.DefaultRowHeight;
|
||||
|
||||
public DynamicUIElement Draw(DrawContext ctx)
|
||||
{
|
||||
var go = LeanPool.Spawn(ctx.Registry.GetPrefab("hintText"), ctx.Parent);
|
||||
var element = go.GetComponent<DynamicUIHintText>();
|
||||
|
||||
element.Initialize(ctx.Target, string.Empty, string.Empty);
|
||||
|
||||
if (ctx.DynamicText != null)
|
||||
{
|
||||
element.SetUpdatingContent(ctx.DynamicText);
|
||||
}
|
||||
else if (ctx.StaticText != null)
|
||||
{
|
||||
element.SetContent(ctx.StaticText);
|
||||
}
|
||||
|
||||
int span = ctx.OverrideSpan ?? DefaultSpan;
|
||||
float height = ctx.OverrideHeight ?? DefaultHeight;
|
||||
element.SetLayoutSize(span * LayoutPacker.TotalRowWidth / LayoutPacker.MaxSpanPerRow, height);
|
||||
|
||||
return element;
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/DynamicUI/Drawers/HintTextDrawer.cs.meta
Normal file
2
Assets/Scripts/DynamicUI/Drawers/HintTextDrawer.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 6437a7c7bdd1fe748b37bb0ebda0abd1
|
||||
42
Assets/Scripts/DynamicUI/Drawers/InputFieldDrawer.cs
Normal file
42
Assets/Scripts/DynamicUI/Drawers/InputFieldDrawer.cs
Normal file
@@ -0,0 +1,42 @@
|
||||
using Ichni.RhythmGame;
|
||||
using Lean.Pool;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
|
||||
namespace Ichni.Editor
|
||||
{
|
||||
/// <summary>
|
||||
/// 处理 float, int, string 类型的 Drawer。
|
||||
/// 创建 DynamicUIInputField 并绑定反射路径。
|
||||
/// </summary>
|
||||
public class InputFieldDrawer : IPropertyDrawer
|
||||
{
|
||||
public int DefaultSpan => 1;
|
||||
public float DefaultHeight => LayoutPacker.DefaultRowHeight;
|
||||
|
||||
public DynamicUIElement Draw(DrawContext ctx)
|
||||
{
|
||||
var go = LeanPool.Spawn(ctx.Registry.GetPrefab("inputField"), ctx.Parent);
|
||||
var element = go.GetComponent<DynamicUIInputField>();
|
||||
|
||||
element.Initialize(ctx.Target, ctx.Label, ctx.FieldName);
|
||||
DynamicUIElement.DisableNavigation(element.inputField);
|
||||
|
||||
if (ctx.AutoUpdate && element is IHaveAutoUpdate autoUpdate)
|
||||
{
|
||||
autoUpdate.SetAutoUpdate(true);
|
||||
}
|
||||
|
||||
if (ctx.ReadOnly)
|
||||
{
|
||||
element.inputField.interactable = false;
|
||||
}
|
||||
|
||||
int span = ctx.OverrideSpan ?? DefaultSpan;
|
||||
float height = ctx.OverrideHeight ?? DefaultHeight;
|
||||
element.SetLayoutSize(span * LayoutPacker.TotalRowWidth / LayoutPacker.MaxSpanPerRow, height);
|
||||
|
||||
return element;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 00072d6a1502bc74d8b7d116cc648bd1
|
||||
35
Assets/Scripts/DynamicUI/Drawers/ParameterTextDrawer.cs
Normal file
35
Assets/Scripts/DynamicUI/Drawers/ParameterTextDrawer.cs
Normal file
@@ -0,0 +1,35 @@
|
||||
using Ichni.RhythmGame;
|
||||
using Lean.Pool;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Ichni.Editor
|
||||
{
|
||||
/// <summary>
|
||||
/// 参数文本 Drawer,绑定字段并显示只读文本,支持自动更新。
|
||||
/// 创建 DynamicUIParameterText。
|
||||
/// </summary>
|
||||
public class ParameterTextDrawer : IPropertyDrawer
|
||||
{
|
||||
public int DefaultSpan => 1;
|
||||
public float DefaultHeight => LayoutPacker.DefaultRowHeight;
|
||||
|
||||
public DynamicUIElement Draw(DrawContext ctx)
|
||||
{
|
||||
var go = LeanPool.Spawn(ctx.Registry.GetPrefab("parameterText"), ctx.Parent);
|
||||
var element = go.GetComponent<DynamicUIParameterText>();
|
||||
|
||||
element.Initialize(ctx.Target, ctx.Label, ctx.FieldName);
|
||||
|
||||
if (ctx.AutoUpdate)
|
||||
{
|
||||
element.SetAutoUpdate(true);
|
||||
}
|
||||
|
||||
int span = ctx.OverrideSpan ?? DefaultSpan;
|
||||
float height = ctx.OverrideHeight ?? DefaultHeight;
|
||||
element.SetLayoutSize(span * LayoutPacker.TotalRowWidth / LayoutPacker.MaxSpanPerRow, height);
|
||||
|
||||
return element;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f9545528a874aff49a39469a79e1860c
|
||||
33
Assets/Scripts/DynamicUI/Drawers/SliderDrawer.cs
Normal file
33
Assets/Scripts/DynamicUI/Drawers/SliderDrawer.cs
Normal file
@@ -0,0 +1,33 @@
|
||||
using Ichni.RhythmGame;
|
||||
using Lean.Pool;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
|
||||
namespace Ichni.Editor
|
||||
{
|
||||
/// <summary>
|
||||
/// 处理带 [InspectorSlider] 属性的 float/int 类型的 Drawer。
|
||||
/// 创建 DynamicUISlider 并配置范围参数。
|
||||
/// </summary>
|
||||
public class SliderDrawer : IPropertyDrawer
|
||||
{
|
||||
public int DefaultSpan => 3;
|
||||
public float DefaultHeight => LayoutPacker.DefaultRowHeight;
|
||||
|
||||
public DynamicUIElement Draw(DrawContext ctx)
|
||||
{
|
||||
var go = LeanPool.Spawn(ctx.Registry.GetPrefab("slider"), ctx.Parent);
|
||||
var element = go.GetComponent<DynamicUISlider>();
|
||||
|
||||
element.Initialize(ctx.Target, ctx.Label, ctx.FieldName,
|
||||
ctx.SliderMin, ctx.SliderMax, ctx.SliderWholeNumbers);
|
||||
DynamicUIElement.DisableNavigation(element.slider);
|
||||
|
||||
int span = ctx.OverrideSpan ?? DefaultSpan;
|
||||
float height = ctx.OverrideHeight ?? DefaultHeight;
|
||||
element.SetLayoutSize(span * LayoutPacker.TotalRowWidth / LayoutPacker.MaxSpanPerRow, height);
|
||||
|
||||
return element;
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/DynamicUI/Drawers/SliderDrawer.cs.meta
Normal file
2
Assets/Scripts/DynamicUI/Drawers/SliderDrawer.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 8740b15baf2fd0147a93e04b4fd41051
|
||||
35
Assets/Scripts/DynamicUI/Drawers/StringListDropdownDrawer.cs
Normal file
35
Assets/Scripts/DynamicUI/Drawers/StringListDropdownDrawer.cs
Normal file
@@ -0,0 +1,35 @@
|
||||
using Ichni.RhythmGame;
|
||||
using Lean.Pool;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Ichni.Editor
|
||||
{
|
||||
/// <summary>
|
||||
/// 处理 List<string> 选择的 Drawer。
|
||||
/// 创建 DynamicUIStringListDropdown 并配置选项列表。
|
||||
/// </summary>
|
||||
public class StringListDropdownDrawer : IPropertyDrawer
|
||||
{
|
||||
public int DefaultSpan => 1;
|
||||
public float DefaultHeight => LayoutPacker.DefaultRowHeight;
|
||||
|
||||
public DynamicUIElement Draw(DrawContext ctx)
|
||||
{
|
||||
var go = LeanPool.Spawn(ctx.Registry.GetPrefab("stringListDropdown"), ctx.Parent);
|
||||
var element = go.GetComponent<DynamicUIStringListDropdown>();
|
||||
|
||||
if (ctx.StringListOptions != null)
|
||||
{
|
||||
element.SetUpStringList(ctx.StringListOptions);
|
||||
}
|
||||
|
||||
element.Initialize(ctx.Target, ctx.Label, ctx.FieldName);
|
||||
|
||||
int span = ctx.OverrideSpan ?? DefaultSpan;
|
||||
float height = ctx.OverrideHeight ?? DefaultHeight;
|
||||
element.SetLayoutSize(span * LayoutPacker.TotalRowWidth / LayoutPacker.MaxSpanPerRow, height);
|
||||
|
||||
return element;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c9f1b1ee81941b94cbb465da262713bf
|
||||
32
Assets/Scripts/DynamicUI/Drawers/ToggleDrawer.cs
Normal file
32
Assets/Scripts/DynamicUI/Drawers/ToggleDrawer.cs
Normal file
@@ -0,0 +1,32 @@
|
||||
using Ichni.RhythmGame;
|
||||
using Lean.Pool;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
|
||||
namespace Ichni.Editor
|
||||
{
|
||||
/// <summary>
|
||||
/// 处理 bool 类型的 Drawer。
|
||||
/// 创建 DynamicUIToggle。
|
||||
/// </summary>
|
||||
public class ToggleDrawer : IPropertyDrawer
|
||||
{
|
||||
public int DefaultSpan => 1;
|
||||
public float DefaultHeight => LayoutPacker.DefaultRowHeight;
|
||||
|
||||
public DynamicUIElement Draw(DrawContext ctx)
|
||||
{
|
||||
var go = LeanPool.Spawn(ctx.Registry.GetPrefab("toggle"), ctx.Parent);
|
||||
var element = go.GetComponent<DynamicUIToggle>();
|
||||
|
||||
element.Initialize(ctx.Target, ctx.Label, ctx.FieldName);
|
||||
DynamicUIElement.DisableNavigation(element.toggle);
|
||||
|
||||
int span = ctx.OverrideSpan ?? DefaultSpan;
|
||||
float height = ctx.OverrideHeight ?? DefaultHeight;
|
||||
element.SetLayoutSize(span * LayoutPacker.TotalRowWidth / LayoutPacker.MaxSpanPerRow, height);
|
||||
|
||||
return element;
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/DynamicUI/Drawers/ToggleDrawer.cs.meta
Normal file
2
Assets/Scripts/DynamicUI/Drawers/ToggleDrawer.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 86988bf7af4e8864094e9fc687e93d29
|
||||
38
Assets/Scripts/DynamicUI/Drawers/Vector2FieldDrawer.cs
Normal file
38
Assets/Scripts/DynamicUI/Drawers/Vector2FieldDrawer.cs
Normal file
@@ -0,0 +1,38 @@
|
||||
using Ichni.RhythmGame;
|
||||
using Lean.Pool;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
|
||||
namespace Ichni.Editor
|
||||
{
|
||||
/// <summary>
|
||||
/// 处理 Vector2 类型的 Drawer。
|
||||
/// 创建 DynamicUIVector2InputField。
|
||||
/// </summary>
|
||||
public class Vector2FieldDrawer : IPropertyDrawer
|
||||
{
|
||||
public int DefaultSpan => 2;
|
||||
public float DefaultHeight => LayoutPacker.DefaultRowHeight;
|
||||
|
||||
public DynamicUIElement Draw(DrawContext ctx)
|
||||
{
|
||||
var go = LeanPool.Spawn(ctx.Registry.GetPrefab("vector2InputField"), ctx.Parent);
|
||||
var element = go.GetComponent<DynamicUIVector2InputField>();
|
||||
|
||||
element.Initialize(ctx.Target, ctx.Label, ctx.FieldName);
|
||||
|
||||
if (ctx.AutoUpdate)
|
||||
{
|
||||
element.SetAutoUpdate(true);
|
||||
}
|
||||
|
||||
DynamicUIElement.DisableNavigation(element.inputFieldX, element.inputFieldY);
|
||||
|
||||
int span = ctx.OverrideSpan ?? DefaultSpan;
|
||||
float height = ctx.OverrideHeight ?? DefaultHeight;
|
||||
element.SetLayoutSize(span * LayoutPacker.TotalRowWidth / LayoutPacker.MaxSpanPerRow, height);
|
||||
|
||||
return element;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 77e741bd7c95a8d4690052986898525e
|
||||
38
Assets/Scripts/DynamicUI/Drawers/Vector3FieldDrawer.cs
Normal file
38
Assets/Scripts/DynamicUI/Drawers/Vector3FieldDrawer.cs
Normal file
@@ -0,0 +1,38 @@
|
||||
using Ichni.RhythmGame;
|
||||
using Lean.Pool;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
|
||||
namespace Ichni.Editor
|
||||
{
|
||||
/// <summary>
|
||||
/// 处理 Vector3 类型的 Drawer。
|
||||
/// 创建 DynamicUIVector3InputField。
|
||||
/// </summary>
|
||||
public class Vector3FieldDrawer : IPropertyDrawer
|
||||
{
|
||||
public int DefaultSpan => 3;
|
||||
public float DefaultHeight => LayoutPacker.DefaultRowHeight;
|
||||
|
||||
public DynamicUIElement Draw(DrawContext ctx)
|
||||
{
|
||||
var go = LeanPool.Spawn(ctx.Registry.GetPrefab("vector3InputField"), ctx.Parent);
|
||||
var element = go.GetComponent<DynamicUIVector3InputField>();
|
||||
|
||||
element.Initialize(ctx.Target, ctx.Label, ctx.FieldName);
|
||||
|
||||
if (ctx.AutoUpdate)
|
||||
{
|
||||
element.SetAutoUpdate(true);
|
||||
}
|
||||
|
||||
DynamicUIElement.DisableNavigation(element.inputFieldX, element.inputFieldY, element.inputFieldZ);
|
||||
|
||||
int span = ctx.OverrideSpan ?? DefaultSpan;
|
||||
float height = ctx.OverrideHeight ?? DefaultHeight;
|
||||
element.SetLayoutSize(span * LayoutPacker.TotalRowWidth / LayoutPacker.MaxSpanPerRow, height);
|
||||
|
||||
return element;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 09da90190e75fd14886f20c784b4bc6f
|
||||
@@ -74,7 +74,6 @@ namespace Ichni.Editor
|
||||
float.Parse(inputFieldBaseB.text), float.Parse(inputFieldBaseA.text));
|
||||
Ichni.Editor.Commands.CommandManager.ExecuteCommand(new Ichni.Editor.Commands.ChangeValueCommand(connectedBaseElement, parameterName, newValue));
|
||||
colorPreview.color = newValue;
|
||||
connectedBaseElement.Refresh();
|
||||
// 同步到HSV轮盘
|
||||
if (hsvDrawer != null)
|
||||
{
|
||||
|
||||
@@ -1,80 +1,123 @@
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
|
||||
using DG.Tweening;
|
||||
using Ichni.RhythmGame;
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Events;
|
||||
using UnityEngine.UI;
|
||||
|
||||
namespace Ichni.Editor
|
||||
{
|
||||
public abstract class DynamicUIElement : MonoBehaviour
|
||||
{
|
||||
Inspector Inspector => EditorManager.instance.uiManager.inspector;
|
||||
|
||||
public TMP_Text title;
|
||||
public CanvasGroup canvasGroup;
|
||||
public IBaseElement connectedBaseElement;
|
||||
|
||||
/// <summary>
|
||||
/// 参数名,通过反射获取饿修改对应变量的值
|
||||
/// </summary>
|
||||
public string parameterName;
|
||||
|
||||
public virtual void Initialize(IBaseElement baseElement, string title, string parameterName)
|
||||
{
|
||||
if (canvasGroup == null) canvasGroup = gameObject.AddComponent<CanvasGroup>();
|
||||
this.connectedBaseElement = baseElement;
|
||||
this.parameterName = parameterName;
|
||||
if (title != string.Empty)
|
||||
{
|
||||
this.title.text = title;
|
||||
}
|
||||
else
|
||||
{
|
||||
this.title.gameObject.SetActive(false);
|
||||
}
|
||||
}
|
||||
|
||||
public DynamicUIElement Mark(string mark = "Default", IHaveInspection inspection = null)
|
||||
{
|
||||
inspection ??= Inspector;
|
||||
if (mark == "Default")
|
||||
{
|
||||
mark = title.text;
|
||||
}
|
||||
|
||||
inspection.MarkedElements.TryAdd(mark, this);
|
||||
return this;
|
||||
}
|
||||
public abstract DynamicUIElement AddListenerFunction(UnityAction action);
|
||||
}
|
||||
|
||||
public interface IHaveAutoUpdate
|
||||
{
|
||||
public bool isAutoUpdate { get; set; }
|
||||
public bool isReceiving { get; set; }
|
||||
public void SetAutoUpdate(bool enable);
|
||||
|
||||
public void UpdateContent()
|
||||
{
|
||||
if (isAutoUpdate && isReceiving)
|
||||
{
|
||||
ApplyContent();
|
||||
}
|
||||
}
|
||||
|
||||
public void ApplyContent();
|
||||
}
|
||||
public interface IHaveTagLink
|
||||
{
|
||||
public IBaseElement connectedBaseElement { get; set; }
|
||||
|
||||
// public void GetApply()
|
||||
// {
|
||||
// EditorManager.instance.projectInformation.tagManager.SyncTagedElement(connectedBaseElement);
|
||||
// }
|
||||
}
|
||||
}
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
|
||||
using DG.Tweening;
|
||||
using Ichni.RhythmGame;
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Events;
|
||||
using UnityEngine.UI;
|
||||
|
||||
namespace Ichni.Editor
|
||||
{
|
||||
public abstract class DynamicUIElement : MonoBehaviour
|
||||
{
|
||||
Inspector Inspector => EditorManager.instance.uiManager.inspector;
|
||||
|
||||
public TMP_Text title;
|
||||
public CanvasGroup canvasGroup;
|
||||
public IBaseElement connectedBaseElement;
|
||||
|
||||
/// <summary>
|
||||
/// 参数名,通过反射获取饿修改对应变量的值
|
||||
/// </summary>
|
||||
public string parameterName;
|
||||
|
||||
/// <summary>
|
||||
/// 缓存的 LayoutElement,用于 InspectorBuilder 的 Flexible 布局模式。
|
||||
/// </summary>
|
||||
private LayoutElement _layoutElement;
|
||||
|
||||
public virtual void Initialize(IBaseElement baseElement, string title, string parameterName)
|
||||
{
|
||||
if (canvasGroup == null) canvasGroup = gameObject.AddComponent<CanvasGroup>();
|
||||
this.connectedBaseElement = baseElement;
|
||||
this.parameterName = parameterName;
|
||||
if (title != string.Empty)
|
||||
{
|
||||
this.title.text = title;
|
||||
}
|
||||
else
|
||||
{
|
||||
this.title.gameObject.SetActive(false);
|
||||
}
|
||||
}
|
||||
|
||||
public DynamicUIElement Mark(string mark = "Default", IHaveInspection inspection = null)
|
||||
{
|
||||
inspection ??= Inspector;
|
||||
if (mark == "Default")
|
||||
{
|
||||
mark = title.text;
|
||||
}
|
||||
|
||||
inspection.MarkedElements.TryAdd(mark, this);
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 设置此元素在 Flexible 布局中的尺寸。
|
||||
/// 由 InspectorBuilder 或 Drawer 在元素创建后调用。
|
||||
/// </summary>
|
||||
public void SetLayoutSize(float preferredWidth, float preferredHeight)
|
||||
{
|
||||
if (_layoutElement == null)
|
||||
_layoutElement = GetComponent<LayoutElement>();
|
||||
if (_layoutElement == null)
|
||||
_layoutElement = gameObject.AddComponent<LayoutElement>();
|
||||
|
||||
_layoutElement.preferredWidth = preferredWidth;
|
||||
_layoutElement.preferredHeight = preferredHeight;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 禁用指定 Selectable 组件的导航(统一处理,避免各 Generate 方法重复编写)。
|
||||
/// </summary>
|
||||
public static void DisableNavigation(Selectable selectable)
|
||||
{
|
||||
if (selectable != null)
|
||||
selectable.navigation = new Navigation { mode = Navigation.Mode.None };
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 批量禁用多个 Selectable 的导航。
|
||||
/// </summary>
|
||||
public static void DisableNavigation(params Selectable[] selectables)
|
||||
{
|
||||
var nav = new Navigation { mode = Navigation.Mode.None };
|
||||
foreach (var s in selectables)
|
||||
{
|
||||
if (s != null)
|
||||
s.navigation = nav;
|
||||
}
|
||||
}
|
||||
|
||||
public abstract DynamicUIElement AddListenerFunction(UnityAction action);
|
||||
}
|
||||
|
||||
public interface IHaveAutoUpdate
|
||||
{
|
||||
public bool isAutoUpdate { get; set; }
|
||||
public bool isReceiving { get; set; }
|
||||
public void SetAutoUpdate(bool enable);
|
||||
|
||||
public void UpdateContent()
|
||||
{
|
||||
if (isAutoUpdate && isReceiving)
|
||||
{
|
||||
ApplyContent();
|
||||
}
|
||||
}
|
||||
|
||||
public void ApplyContent();
|
||||
}
|
||||
public interface IHaveTagLink
|
||||
{
|
||||
public IBaseElement connectedBaseElement { get; set; }
|
||||
|
||||
// public void GetApply()
|
||||
// {
|
||||
// EditorManager.instance.projectInformation.tagManager.SyncTagedElement(connectedBaseElement);
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -97,7 +97,6 @@ namespace Ichni.Editor
|
||||
Ichni.Editor.Commands.CommandManager.ExecuteCommand(new Ichni.Editor.Commands.ChangeValueCommand(connectedBaseElement, colorParameterName, emissionColor));
|
||||
|
||||
colorPreview.color = emissionColor;
|
||||
connectedBaseElement.Refresh();
|
||||
}
|
||||
|
||||
public void SliderChange(float value)
|
||||
|
||||
@@ -49,7 +49,6 @@ namespace Ichni.Editor
|
||||
private void ApplyParameters(int value)
|
||||
{
|
||||
Ichni.Editor.Commands.CommandManager.ExecuteCommand(new Ichni.Editor.Commands.ChangeValueCommand(connectedBaseElement, parameterName, value));
|
||||
connectedBaseElement.Refresh();
|
||||
}
|
||||
|
||||
public override DynamicUIElement AddListenerFunction(UnityAction action)
|
||||
|
||||
@@ -113,7 +113,6 @@ namespace Ichni.Editor
|
||||
{
|
||||
Ichni.Editor.Commands.CommandManager.ExecuteCommand(new Ichni.Editor.Commands.ChangeValueCommand(connectedBaseElement, parameterName, value));
|
||||
}
|
||||
connectedBaseElement.Refresh();
|
||||
}
|
||||
|
||||
public override DynamicUIElement AddListenerFunction(UnityAction action)
|
||||
|
||||
@@ -156,7 +156,6 @@ namespace Ichni.Editor
|
||||
}
|
||||
Ichni.Editor.Commands.CommandManager.ExecuteCommand(
|
||||
new Ichni.Editor.Commands.ChangeValueCommand(connectedBaseElement, parameterName, convertedValue));
|
||||
connectedBaseElement.Refresh();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
@@ -76,7 +76,6 @@ namespace Ichni.Editor
|
||||
private void ApplyParameters(string value)
|
||||
{
|
||||
Ichni.Editor.Commands.CommandManager.ExecuteCommand(new Ichni.Editor.Commands.ChangeValueCommand(connectedBaseElement, parameterName, value));
|
||||
connectedBaseElement.Refresh();
|
||||
}
|
||||
|
||||
public override DynamicUIElement AddListenerFunction(UnityAction action)
|
||||
|
||||
@@ -53,7 +53,6 @@ namespace Ichni.Editor
|
||||
private void ApplyParameters(bool value)
|
||||
{
|
||||
Ichni.Editor.Commands.CommandManager.ExecuteCommand(new Ichni.Editor.Commands.ChangeValueCommand(connectedBaseElement, parameterName, value));
|
||||
connectedBaseElement.Refresh();
|
||||
}
|
||||
|
||||
public override DynamicUIElement AddListenerFunction(UnityAction action)
|
||||
|
||||
@@ -116,7 +116,6 @@ namespace Ichni.Editor
|
||||
{
|
||||
Vector2 newValue = new Vector2(float.Parse(inputFieldX.text), float.Parse(inputFieldY.text));
|
||||
Ichni.Editor.Commands.CommandManager.ExecuteCommand(new Ichni.Editor.Commands.ChangeValueCommand(connectedBaseElement, parameterName, newValue));
|
||||
connectedBaseElement.Refresh();
|
||||
}
|
||||
|
||||
public override DynamicUIElement AddListenerFunction(UnityAction action)
|
||||
|
||||
@@ -135,7 +135,6 @@ namespace Ichni.Editor
|
||||
{
|
||||
Vector3 newValue = new Vector3(float.Parse(inputFieldX.text), float.Parse(inputFieldY.text), float.Parse(inputFieldZ.text));
|
||||
Ichni.Editor.Commands.CommandManager.ExecuteCommand(new Ichni.Editor.Commands.ChangeValueCommand(connectedBaseElement, parameterName, newValue));
|
||||
connectedBaseElement.Refresh();
|
||||
}
|
||||
|
||||
public override DynamicUIElement AddListenerFunction(UnityAction action)
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
@@ -9,21 +8,64 @@ namespace Ichni.Editor
|
||||
{
|
||||
Inspector Inspector => EditorManager.instance.uiManager.inspector;
|
||||
public DynamicUIContainer parentContainer;
|
||||
public GridLayoutGroup gridLayoutGroup;
|
||||
public RectTransform rect;
|
||||
public List<DynamicUIElement> dynamicUIElements;
|
||||
|
||||
/// <summary> 当前行高 </summary>
|
||||
[HideInInspector]
|
||||
public float rowHeight = 100f;
|
||||
|
||||
/// <summary>
|
||||
/// 使用 GridLayoutGroup 实现 N 列自动换行布局。
|
||||
/// 当添加的元素数量超过 elementCountPerRow 时,会自动换行到下一行。
|
||||
/// </summary>
|
||||
public void Initialize(DynamicUIContainer parentContainer, int elementCountPerRow, float height = 100)
|
||||
{
|
||||
this.parentContainer = parentContainer;
|
||||
this.gridLayoutGroup.cellSize = new Vector2(600f / elementCountPerRow, height);
|
||||
// 支持对象池复用:避免每次 new List 产生 GC,直接清空
|
||||
if (this.dynamicUIElements == null)
|
||||
this.dynamicUIElements = new List<DynamicUIElement>();
|
||||
this.rowHeight = height;
|
||||
|
||||
int columns = Mathf.Max(1, elementCountPerRow);
|
||||
|
||||
// ── 启用 GridLayoutGroup ──
|
||||
var grid = GetComponent<GridLayoutGroup>();
|
||||
if (grid == null)
|
||||
grid = gameObject.AddComponent<GridLayoutGroup>();
|
||||
|
||||
grid.enabled = true;
|
||||
grid.constraint = GridLayoutGroup.Constraint.FixedColumnCount;
|
||||
grid.constraintCount = columns;
|
||||
grid.childAlignment = TextAnchor.UpperLeft;
|
||||
grid.startCorner = GridLayoutGroup.Corner.UpperLeft;
|
||||
grid.startAxis = GridLayoutGroup.Axis.Horizontal;
|
||||
grid.padding = new RectOffset(10, 10, 0, 0);
|
||||
|
||||
float spacing = 10f;
|
||||
float totalSpacing = (columns - 1) * spacing;
|
||||
float cellWidth = (LayoutPacker.TotalRowWidth - totalSpacing) / columns;
|
||||
grid.cellSize = new Vector2(cellWidth, height);
|
||||
grid.spacing = new Vector2(spacing, spacing);
|
||||
|
||||
// ── LayoutElement: preferredHeight = -1 让 GridLayoutGroup 的 ILayoutElement 接管高度 ──
|
||||
var layoutElement = GetComponent<LayoutElement>();
|
||||
if (layoutElement == null)
|
||||
layoutElement = gameObject.AddComponent<LayoutElement>();
|
||||
layoutElement.preferredHeight = -1;
|
||||
layoutElement.flexibleHeight = -1;
|
||||
|
||||
// ── ContentSizeFitter: 让子容器高度随网格行数动态增长 ──
|
||||
var fitter = GetComponent<ContentSizeFitter>();
|
||||
if (fitter == null)
|
||||
fitter = gameObject.AddComponent<ContentSizeFitter>();
|
||||
fitter.verticalFit = ContentSizeFitter.FitMode.PreferredSize;
|
||||
fitter.horizontalFit = ContentSizeFitter.FitMode.Unconstrained;
|
||||
fitter.enabled = true;
|
||||
|
||||
if (dynamicUIElements == null)
|
||||
dynamicUIElements = new List<DynamicUIElement>();
|
||||
else
|
||||
this.dynamicUIElements.Clear();
|
||||
dynamicUIElements.Clear();
|
||||
}
|
||||
|
||||
|
||||
public DynamicUISubcontainer Mark(string mark, IHaveInspection inspection = null)
|
||||
{
|
||||
inspection ??= Inspector;
|
||||
@@ -31,4 +73,4 @@ namespace Ichni.Editor
|
||||
return this;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,98 +0,0 @@
|
||||
using System;
|
||||
|
||||
namespace Ichni.Editor
|
||||
{
|
||||
/// <summary>
|
||||
/// 数值或枚举控制参数标签,挂载在 Field 或 Property 上
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property)]
|
||||
public class DynamicUIAttribute : Attribute
|
||||
{
|
||||
public string Name { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 属于哪个 Container (比如 "InGame Settings" 或 "Path Node")
|
||||
/// </summary>
|
||||
public string Group { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 排版格子要求。一行最大宽度分为 3 份。
|
||||
/// 填 1 代表需占 1/3。填 3 代表全宽占满整行。
|
||||
/// 若填 0,由内置工厂根据数据类型自动识别(例如 Vector3 与 Color 自动为 3,bool为1 等)。
|
||||
/// </summary>
|
||||
public int Span { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 垂直占位高度。填 0 由系统智能推断(如基础件 100,ColorPicker 240),也可显式填写如 150f 覆盖。
|
||||
/// </summary>
|
||||
public float Height { get; set; }
|
||||
|
||||
public bool AutoUpdate { get; set; }
|
||||
public bool ReadOnly { get; set; }
|
||||
|
||||
public DynamicUIAttribute(string name = "", string group = "Default", int span = 0, float height = 0f, bool autoUpdate = false, bool readOnly = false)
|
||||
{
|
||||
Name = name;
|
||||
Group = group;
|
||||
Span = span;
|
||||
Height = height;
|
||||
AutoUpdate = autoUpdate;
|
||||
ReadOnly = readOnly;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 支持 HDR 以及独立 Emission 强度或开关控制通道的拾色器。
|
||||
/// 若无独立通道字段可置为 "NULL",将自动转为读取并写入至 Target Color 的 Alpha 通道中。
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property)]
|
||||
public class DynamicUIEmissionColorAttribute : DynamicUIAttribute
|
||||
{
|
||||
public string EmissionEnabledName { get; set; }
|
||||
public string EmissionIntensityName { get; set; }
|
||||
|
||||
public DynamicUIEmissionColorAttribute(string name, string enabledName = "NULL", string intensityName = "NULL", string group = "Default", int span = 3, float height = 0f)
|
||||
: base(name, group, span, height)
|
||||
{
|
||||
EmissionEnabledName = enabledName;
|
||||
EmissionIntensityName = intensityName;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 滑块控制属性,适用于 float/int 类型字段,自动生成带范围限制的 Slider 替代 InputField。
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property)]
|
||||
public class DynamicUISliderAttribute : DynamicUIAttribute
|
||||
{
|
||||
public float Min { get; set; }
|
||||
public float Max { get; set; }
|
||||
public bool WholeNumbers { get; set; }
|
||||
|
||||
public DynamicUISliderAttribute(string name = "", string group = "Default",
|
||||
float min = 0f, float max = 1f, bool wholeNumbers = false,
|
||||
int span = 0, float height = 0f, bool autoUpdate = false, bool readOnly = false)
|
||||
: base(name, group, span, height, autoUpdate, readOnly)
|
||||
{
|
||||
Min = min;
|
||||
Max = max;
|
||||
WholeNumbers = wholeNumbers;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 操作按钮卡片,挂载在没有任何参数的 Method() 方法上
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Method)]
|
||||
public class DynamicUIButtonAttribute : Attribute
|
||||
{
|
||||
public string Name { get; set; }
|
||||
public string Group { get; set; }
|
||||
|
||||
public DynamicUIButtonAttribute(string name = "", string group = "Default")
|
||||
{
|
||||
Name = name;
|
||||
Group = group;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 8a540054c95f48544bd6efcea6b452cc
|
||||
@@ -1,228 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using UnityEngine;
|
||||
using Ichni.RhythmGame;
|
||||
using UnityEngine.Events;
|
||||
|
||||
namespace Ichni.Editor
|
||||
{
|
||||
public static class DynamicUIAutoBuilder
|
||||
{
|
||||
// 反射信息快取
|
||||
private class FieldDef
|
||||
{
|
||||
public MemberInfo Member;
|
||||
public DynamicUIAttribute Attr;
|
||||
public Type ValueType;
|
||||
}
|
||||
|
||||
private class MethodDef
|
||||
{
|
||||
public MethodInfo Method;
|
||||
public DynamicUIButtonAttribute Attr;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 接管任何 IBaseElement 的 UI 生成作业
|
||||
/// </summary>
|
||||
public static void AutoBuild(IBaseElement element, IHaveInspection inspector)
|
||||
{
|
||||
Type t = element.GetType();
|
||||
|
||||
// 扫描所有带 DynamicUI 标记的字段或属性
|
||||
var dynamicFields = new List<FieldDef>();
|
||||
foreach (var member in t.GetMembers(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic))
|
||||
{
|
||||
var uiAttr = member.GetCustomAttribute<DynamicUIAttribute>();
|
||||
if (uiAttr != null)
|
||||
{
|
||||
Type vType = member is FieldInfo f ? f.FieldType : (member as PropertyInfo)?.PropertyType;
|
||||
if (vType != null) dynamicFields.Add(new FieldDef { Member = member, Attr = uiAttr, ValueType = vType });
|
||||
}
|
||||
}
|
||||
|
||||
// 扫描所有带 DynamicUIButton 的方法
|
||||
var dynamicMethods = new List<MethodDef>();
|
||||
foreach (var method in t.GetMethods(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic))
|
||||
{
|
||||
var btnAttr = method.GetCustomAttribute<DynamicUIButtonAttribute>();
|
||||
if (btnAttr != null && method.GetParameters().Length == 0)
|
||||
{
|
||||
dynamicMethods.Add(new MethodDef { Method = method, Attr = btnAttr });
|
||||
}
|
||||
}
|
||||
|
||||
// 按照 Group 归类准备流式渲染
|
||||
var allGroups = dynamicFields.Select(f => f.Attr.Group)
|
||||
.Concat(dynamicMethods.Select(m => m.Attr.Group))
|
||||
.Distinct().ToList();
|
||||
|
||||
foreach (var groupName in allGroups)
|
||||
{
|
||||
var container = inspector.GenerateContainer(groupName);
|
||||
|
||||
var fieldsInGroup = dynamicFields.Where(f => f.Attr.Group == groupName).ToList();
|
||||
var methodsInGroup = dynamicMethods.Where(m => m.Attr.Group == groupName).ToList();
|
||||
|
||||
// ======== 智能栅格打包推演 ========
|
||||
List<FieldDef> currentLineBuffer = new List<FieldDef>();
|
||||
int currentSpanCount = 0; // 一行容量最高是 3
|
||||
|
||||
// 先排变量
|
||||
foreach (var field in fieldsInGroup)
|
||||
{
|
||||
int span = GetEffectiveSpan(field);
|
||||
span = Mathf.Clamp(span, 1, 3);
|
||||
|
||||
// 如果该行的槽被塞满了或者装不下了
|
||||
if (currentSpanCount > 0 && currentSpanCount + span > 3)
|
||||
{
|
||||
FlushLine(element, inspector, container, currentLineBuffer, 3);
|
||||
currentLineBuffer.Clear();
|
||||
currentSpanCount = 0;
|
||||
}
|
||||
|
||||
// 特殊兼容:由于 GridLayoutGroup 的强制统一 cellSize, 我们如果在同一行混搭不同 Span 宽度的,反而会让它变形。
|
||||
// 因此若当前这行的空间要求和前一个需求跨度不一致,也必须强行断点分层!
|
||||
if (currentLineBuffer.Count > 0)
|
||||
{
|
||||
var lastSpan = GetEffectiveSpan(currentLineBuffer[0]);
|
||||
if (span != lastSpan)
|
||||
{
|
||||
FlushLine(element, inspector, container, currentLineBuffer, GetHighestSpanRequirement(currentLineBuffer));
|
||||
currentLineBuffer.Clear();
|
||||
currentSpanCount = 0;
|
||||
}
|
||||
}
|
||||
|
||||
currentLineBuffer.Add(field);
|
||||
currentSpanCount += span;
|
||||
}
|
||||
if (currentLineBuffer.Count > 0) FlushLine(element, inspector, container, currentLineBuffer, GetHighestSpanRequirement(currentLineBuffer));
|
||||
|
||||
// ======== 最后把按钮并排排好 ========
|
||||
if (methodsInGroup.Count > 0)
|
||||
{
|
||||
var btnSubcontainer = container.GenerateSubcontainer(3); // 按钮统统放在 1/3 的槽里(你可改规则)
|
||||
foreach (var method in methodsInGroup)
|
||||
{
|
||||
MethodInfo mInfo = method.Method;
|
||||
// 通过强行抓取生成 UnityAction 给工厂
|
||||
UnityAction action = () => { mInfo.Invoke(element, null); element.Refresh(); };
|
||||
string n = string.IsNullOrEmpty(method.Attr.Name) ? mInfo.Name : method.Attr.Name;
|
||||
inspector.GenerateButton(element, btnSubcontainer, n, action);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 核心工厂发射器
|
||||
private static void FlushLine(IBaseElement element, IHaveInspection inspector, DynamicUIContainer container, List<FieldDef> fields, int lineSpanCapacity)
|
||||
{
|
||||
// 如果一行要放 3 份(Span 都是 1 拼成的),那格子数量也是 3(600/3=200一个)
|
||||
// 但是如果是独占的(Span 都是 3),格子数量反而应该是 1(600/1=600一个!)
|
||||
// 为了防止排版错开变形,我们通过 3 / span 来决定 Subcontainer 内部允许的等分元素数量。
|
||||
int subcontainerDivision = Mathf.Max(1, Mathf.RoundToInt(3f / lineSpanCapacity));
|
||||
|
||||
float rowHeight = GetHighestHeightRequirement(fields);
|
||||
var sub = container.GenerateSubcontainer(subcontainerDivision, rowHeight);
|
||||
|
||||
foreach (var fd in fields)
|
||||
{
|
||||
string title = string.IsNullOrEmpty(fd.Attr.Name) ? fd.Member.Name : fd.Attr.Name;
|
||||
string pName = fd.Member.Name;
|
||||
|
||||
if (fd.Attr.ReadOnly)
|
||||
{
|
||||
var val = ReflectionHelper.GetDeepValue(element, pName);
|
||||
inspector.GenerateHintText(element, sub, $"{title}: {val}");
|
||||
continue;
|
||||
}
|
||||
|
||||
// 智能类型分流
|
||||
if (fd.Attr is DynamicUIEmissionColorAttribute emissionAttr)
|
||||
{
|
||||
// 使用之前为了消除扩展方法调不出来的尴尬而专门暴露的强转/普通调用,我们直接这里也安全使用
|
||||
IHaveInspection rawInspector = inspector as IHaveInspection;
|
||||
if(rawInspector != null) rawInspector.GenerateEmissionColorPicker(element, sub, title, emissionAttr.EmissionEnabledName, pName, emissionAttr.EmissionIntensityName);
|
||||
}
|
||||
else if (fd.ValueType == typeof(bool))
|
||||
{
|
||||
inspector.GenerateToggle(element, sub, title, pName);
|
||||
}
|
||||
else if (fd.ValueType == typeof(Vector2))
|
||||
{
|
||||
inspector.GenerateVector2InputField(element, sub, title, pName, fd.Attr.AutoUpdate);
|
||||
}
|
||||
else if (fd.ValueType == typeof(Vector3))
|
||||
{
|
||||
inspector.GenerateVector3InputField(element, sub, title, pName, fd.Attr.AutoUpdate);
|
||||
}
|
||||
else if (fd.ValueType == typeof(Color))
|
||||
{
|
||||
inspector.GenerateBaseColorPicker(element, sub, title, pName);
|
||||
}
|
||||
else if (fd.ValueType.IsEnum)
|
||||
{
|
||||
inspector.GenerateDropdown(element, sub, title, fd.ValueType, pName);
|
||||
}
|
||||
else if (fd.Attr is DynamicUISliderAttribute sliderAttr)
|
||||
{
|
||||
inspector.GenerateSlider(element, sub, title, pName, sliderAttr.Min, sliderAttr.Max, sliderAttr.WholeNumbers);
|
||||
}
|
||||
else // 容错给普通的 InputField
|
||||
{
|
||||
inspector.GenerateInputField(element, sub, title, pName, fd.Attr.AutoUpdate);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static int SmartInferSpan(Type t)
|
||||
{
|
||||
if (t == typeof(bool)) return 1;
|
||||
if (t == typeof(float) || t == typeof(int) || t == typeof(string)) return 1;
|
||||
if (t.IsEnum) return 1;
|
||||
if (t == typeof(Vector2)) return 2;
|
||||
if (t == typeof(Vector3) || t == typeof(Color)) return 3; // 巨型组件通篇独占一行
|
||||
return 1;
|
||||
}
|
||||
|
||||
private static int GetHighestSpanRequirement(List<FieldDef> fields)
|
||||
{
|
||||
int highest = 1;
|
||||
foreach (var f in fields)
|
||||
{
|
||||
int span = GetEffectiveSpan(f);
|
||||
if (span > highest) highest = span;
|
||||
}
|
||||
return highest;
|
||||
}
|
||||
|
||||
private static int GetEffectiveSpan(FieldDef fd)
|
||||
{
|
||||
return fd.Attr.Span > 0 ? fd.Attr.Span : SmartInferSpan(fd.ValueType);
|
||||
}
|
||||
|
||||
private static float SmartInferHeight(FieldDef fd)
|
||||
{
|
||||
// EmissionColorPicker 有 Toggle/RGB滑块/Intensity输入框,高度明显大于 BaseColorPicker
|
||||
if (fd.Attr is DynamicUIEmissionColorAttribute) return 320f;
|
||||
if (fd.ValueType == typeof(Color)) return 280f;
|
||||
// 多数基础组件默认为 100
|
||||
return 100f;
|
||||
}
|
||||
|
||||
private static float GetHighestHeightRequirement(List<FieldDef> fields)
|
||||
{
|
||||
float highest = 100f;
|
||||
foreach (var f in fields)
|
||||
{
|
||||
float height = f.Attr.Height > 0 ? f.Attr.Height : SmartInferHeight(f);
|
||||
if (height > highest) highest = height;
|
||||
}
|
||||
return highest;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f0bc252acb576104aaff0846e4c1e136
|
||||
@@ -13,6 +13,11 @@ using Object = UnityEngine.Object;
|
||||
|
||||
namespace Ichni.Editor
|
||||
{
|
||||
/// <summary>
|
||||
/// Inspector 工厂接口 —— 提供底层 UI 控件的实例化方法。
|
||||
/// 外部代码应通过 <see cref="InspectorBuilder"/> 构建 Inspector,
|
||||
/// 而非直接调用本接口的 Generate 方法。
|
||||
/// </summary>
|
||||
public interface IHaveInspection
|
||||
{
|
||||
public RectTransform WindowRect { get; set; }
|
||||
@@ -226,6 +231,7 @@ namespace Ichni.Editor
|
||||
return hintText;
|
||||
}
|
||||
|
||||
[Obsolete("GenerateParameterText 已无外部调用者,请使用 InspectorBuilder.HintText() 替代。")]
|
||||
public DynamicUIParameterText GenerateParameterText(IBaseElement baseElement, DynamicUISubcontainer subcontainer,
|
||||
string title, string parameterName, bool isAutoUpdate = false)
|
||||
{
|
||||
|
||||
@@ -22,6 +22,9 @@ namespace Ichni.Editor
|
||||
public Dictionary<string, DynamicUISubcontainer> MarkedSubcontainers { get; set; }
|
||||
public Dictionary<string, DynamicUIElement> MarkedElements { get; set; }
|
||||
|
||||
/// <summary> Drawer 注册表单例的便捷访问 </summary>
|
||||
public DrawerRegistry DrawerRegistry => DrawerRegistry.Instance;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
WindowRect = inspectorRect;
|
||||
|
||||
@@ -1,59 +0,0 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using Ichni.RhythmGame;
|
||||
using UnityEngine;
|
||||
namespace Ichni.Editor
|
||||
{
|
||||
public static class StandardInspectionElement
|
||||
{
|
||||
private static IHaveInspection inspector => EditorManager.instance.uiManager.inspector;
|
||||
private static Inspector inspectorUI => EditorManager.instance.uiManager.inspector;
|
||||
public static void GenerateForTransform(GameElement gameElement, DynamicUIContainer generateContainer = null)//关于有Transform
|
||||
{
|
||||
if (generateContainer is null)
|
||||
{
|
||||
generateContainer = inspector.GenerateContainer("Generate Elements");
|
||||
}
|
||||
var animationSubcontainer = generateContainer.GenerateSubcontainer(3);
|
||||
var displacementButton = inspector.GenerateButton(gameElement, animationSubcontainer, "Displacement", () =>
|
||||
{
|
||||
Displacement.GenerateElement("New Displacement", Guid.NewGuid(), new List<string>(), true, gameElement,
|
||||
new FlexibleFloat(true), new FlexibleFloat(true), new FlexibleFloat(true));
|
||||
}); //位移
|
||||
|
||||
var swirlButton = inspector.GenerateButton(gameElement, animationSubcontainer, "Swirl", () =>
|
||||
{
|
||||
Swirl.GenerateElement("New Swirl", Guid.NewGuid(), new List<string>(), true, gameElement,
|
||||
new FlexibleFloat(true), new FlexibleFloat(true), new FlexibleFloat(true));
|
||||
}); //旋转
|
||||
var scaleButton = inspector.GenerateButton(gameElement, animationSubcontainer, "Scale", () =>
|
||||
{
|
||||
Scale.GenerateElement("New Scale", Guid.NewGuid(), new List<string>(), true, gameElement,
|
||||
new FlexibleFloat(true), new FlexibleFloat(true), new FlexibleFloat(true));
|
||||
}); //缩放
|
||||
var LookAtButton = inspector.GenerateButton(gameElement, animationSubcontainer, "Look At",
|
||||
() => LookAt.GenerateElement("New Look At", Guid.NewGuid(),
|
||||
new List<string>(), true, gameElement, null, new FlexibleBool()));
|
||||
var displacementTrackerButton = inspector.GenerateButton(gameElement, animationSubcontainer, "Displacement Tracker", () =>
|
||||
{
|
||||
DisplacementTracker.GenerateElement("New Displacement Tracker", Guid.NewGuid(), new List<string>(), true, gameElement,
|
||||
null, 0f);
|
||||
});
|
||||
var swirlTrackerButton = inspector.GenerateButton(gameElement, animationSubcontainer, "Swirl Tracker", () =>
|
||||
{
|
||||
SwirlTracker.GenerateElement("New Swirl Tracker", Guid.NewGuid(), new List<string>(), true, gameElement,
|
||||
null, 0f);
|
||||
}); var ScaleTrackerButton = inspector.GenerateButton(gameElement, animationSubcontainer, "Scale Tracker", () =>
|
||||
{
|
||||
ScaleTracker.GenerateElement("New Scale Tracker", Guid.NewGuid(), new List<string>(), true, gameElement,
|
||||
null, 0f);
|
||||
});
|
||||
}
|
||||
public static void GenerateForLoading()
|
||||
{
|
||||
inspectorUI.ClearInspector();
|
||||
var container = inspector.GenerateContainer("Loading");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 97779e95563721b4e8d4a28bb0d46cb9
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Reference in New Issue
Block a user