From 251b8730b977c442edd8e2b291dcb8c7a12a7e71 Mon Sep 17 00:00:00 2001 From: lrc <571244399@qq.com> Date: Sat, 25 Jul 2026 23:38:49 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96ui=E7=BB=91=E5=AE=9A=E4=BE=A7?= =?UTF-8?q?=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Scripts/AvrgBindingTrie.cs | 77 ++++++ .../Scripts/AvrgMemberMetaDataSet.cs | 16 ++ .../Scripts/AvrgMvvmBinding.cs | 64 ++++- .../Scripts/AvrgMvvmBindingCollection.cs | 48 +++- .../Scripts/AvrgMvvmScheduler.cs | 123 ++++++--- .../Scripts/AvrgSubPropertyAccessor.cs | 79 ++++++ .../Scripts/ValidationState.cs | 13 + .../Scripts/XuiFieldTempEntry.cs | 4 +- .../Scripts/XuiavrgInspectorManager.cs | 14 +- .../AvrgValues/XUIAvrgMDColorValue.cs | 37 +-- .../AvrgValues/XUIAvrgMDDropdownValue.cs | 61 +++-- .../AvrgValues/XUIAvrgMDInputFieldValue.cs | 71 +++-- .../AvrgValues/XUIAvrgMDLabelValue.cs | 44 +--- .../AvrgValues/XUIAvrgMDSliderValue.cs | 105 ++++---- .../AvrgValues/XUIAvrgMDTextureValue.cs | 51 ++-- .../AvrgValues/XUIAvrgMDToggleGroupValue.cs | 11 +- .../AvrgValues/XUIAvrgMDToggleValue.cs | 69 ++--- .../OverrideUI/AvrgValues/XUIAvrgMDValue.cs | 247 ++++++++++-------- .../AvrgValues/XUIAvrgMDVectorValue.cs | 84 ++---- 19 files changed, 782 insertions(+), 436 deletions(-) create mode 100644 Runtime/InterfaceAttributeReflectionGenerator/Scripts/AvrgBindingTrie.cs create mode 100644 Runtime/InterfaceAttributeReflectionGenerator/Scripts/AvrgSubPropertyAccessor.cs create mode 100644 Runtime/InterfaceAttributeReflectionGenerator/Scripts/ValidationState.cs diff --git a/Runtime/InterfaceAttributeReflectionGenerator/Scripts/AvrgBindingTrie.cs b/Runtime/InterfaceAttributeReflectionGenerator/Scripts/AvrgBindingTrie.cs new file mode 100644 index 0000000..feeb9e1 --- /dev/null +++ b/Runtime/InterfaceAttributeReflectionGenerator/Scripts/AvrgBindingTrie.cs @@ -0,0 +1,77 @@ +using System.Collections.Generic; + +namespace XericUI.ReflectionUIGenerator +{ + /// + /// 绑定名称前缀树(Trie),支持通过完整路径或成员名后缀快速查找 AvrgMvvmBinding。 + /// + /// 使用场景: + /// - Find("XericUI.MyData.speed") → 完整路径精确命中 + /// - FindByMemberName("speed") → 沿 Trie 叶节点查找末段匹配 + /// + /// 在 BuildMDVM 时构建,绑定到 AvrgMvvmBindingCollection。 + /// + internal class AvrgBindingTrie + { + private class Node + { + public AvrgMvvmBinding Binding; + public readonly Dictionary Children = new(); + } + + private readonly Node _root = new(); + private readonly Dictionary _memberNameIndex = new(); + + public void Insert(string fullPath, AvrgMvvmBinding binding) + { + if (string.IsNullOrEmpty(fullPath) || binding == null) return; + + var parts = fullPath.Split('.'); + var current = _root; + foreach (var part in parts) + { + if (!current.Children.TryGetValue(part, out var child)) + { + child = new Node(); + current.Children[part] = child; + } + current = child; + } + current.Binding = binding; + + // 同时建成员名索引(末段) + string memberName = parts[parts.Length - 1]; + if (!_memberNameIndex.ContainsKey(memberName)) + _memberNameIndex[memberName] = binding; + } + + /// 通过完整路径精确查找(如 "XericUI.MyData.speed") + public AvrgMvvmBinding Find(string fullPath) + { + if (string.IsNullOrEmpty(fullPath)) return null; + + var parts = fullPath.Split('.'); + var current = _root; + foreach (var part in parts) + { + if (!current.Children.TryGetValue(part, out current)) + return null; + } + return current?.Binding; + } + + /// 通过成员名查找(如 "speed")— O(1) 直接索引 + public AvrgMvvmBinding FindByMemberName(string memberName) + { + _memberNameIndex.TryGetValue(memberName, out var binding); + return binding; + } + + /// 清除所有索引 + public void Clear() + { + _root.Children.Clear(); + _memberNameIndex.Clear(); + } + } +} diff --git a/Runtime/InterfaceAttributeReflectionGenerator/Scripts/AvrgMemberMetaDataSet.cs b/Runtime/InterfaceAttributeReflectionGenerator/Scripts/AvrgMemberMetaDataSet.cs index 56be430..9c1fde4 100644 --- a/Runtime/InterfaceAttributeReflectionGenerator/Scripts/AvrgMemberMetaDataSet.cs +++ b/Runtime/InterfaceAttributeReflectionGenerator/Scripts/AvrgMemberMetaDataSet.cs @@ -91,6 +91,12 @@ namespace XericUI.ReflectionUIGenerator bindingCollection.SourceMetaDataSet = this; var scheduler = new AvrgMvvmScheduler(resolverConfig); + // ★ 构建前缀树索引 + var trie = new AvrgBindingTrie(); + string typePrefix = businessTarget != null + ? businessTarget.GetType().FullName + "." + : ""; + foreach (var metaData in _metaDataList) { if (metaData.AccessorFactory == null) continue; @@ -137,9 +143,14 @@ namespace XericUI.ReflectionUIGenerator string subPath = subMetaData.PropertyPath; if (string.IsNullOrEmpty(subPath)) continue; + // 创建子属性访问器 + var subAccessor = new AvrgSubPropertyAccessor( + accessor, subPath, subMetaData); + var subBinding = new AvrgMvvmBinding { MetaData = subMetaData, + Accessor = subAccessor, ReadFunc = () => ReadSubProperty(accessor.GetValue(), subPath), WriteAction = (val) => { @@ -157,8 +168,13 @@ namespace XericUI.ReflectionUIGenerator // 注册到调度器 scheduler.Register(binding); bindingCollection.Add(binding); + + // ★ 插入前缀树(完整路径: Type.FullName.MemberName) + string fullPath = typePrefix + metaData.MemberName; + trie.Insert(fullPath, binding); } + bindingCollection.SetTrie(trie); bindingCollection.AttachScheduler(scheduler); return bindingCollection; } diff --git a/Runtime/InterfaceAttributeReflectionGenerator/Scripts/AvrgMvvmBinding.cs b/Runtime/InterfaceAttributeReflectionGenerator/Scripts/AvrgMvvmBinding.cs index c8faa0f..757e32b 100644 --- a/Runtime/InterfaceAttributeReflectionGenerator/Scripts/AvrgMvvmBinding.cs +++ b/Runtime/InterfaceAttributeReflectionGenerator/Scripts/AvrgMvvmBinding.cs @@ -45,21 +45,75 @@ namespace XericUI.ReflectionUIGenerator /// 数据侧写入的缓存值 public object DataCachedValue { get; set; } + // ============ 验证状态 ============ + + private ValidationState _validationState = ValidationState.Normal; + + /// 当前验证状态 + public ValidationState CurrentValidationState => _validationState; + + /// 验证状态变更事件 — UI 组件订阅以切换样式 + public event Action OnValidationStateChanged; + + /// + /// 校验值是否满足元数据约束 + /// 返回:Normal(通过)、Warning(放行但警告)、Invalid(驳回) + /// + public ValidationState ValidateValue(object value) + { + if (MetaData == null) return ValidationState.Normal; + + // 1. 空值检查(Required) + if (MetaData.Required && (value == null || (value is string s && string.IsNullOrEmpty(s)))) + return ValidationState.Invalid; + + // 2. 范围检查(RangeMin/RangeMax) + if (value is IConvertible) + { + float num = Convert.ToSingle(value); + + if (MetaData.RangeMin.HasValue && num < MetaData.RangeMin.Value) + return ValidationState.Invalid; + + if (MetaData.RangeMax.HasValue && num > MetaData.RangeMax.Value) + return ValidationState.Invalid; + } + + return ValidationState.Normal; + } + + /// 更新验证状态并触发回调(仅状态变化时触发) + internal void SetValidationState(ValidationState newState) + { + if (_validationState != newState) + { + _validationState = newState; + OnValidationStateChanged?.Invoke(_validationState); + } + } + + /// 重置验证状态为 Normal(不触发事件) + internal void ResetValidationState() + { + _validationState = ValidationState.Normal; + } + // ============ UI 绑定 ============ - /// UI 值变更回调委托 — UI 组件订阅此回调以更新显示 - public Action OnUIDataChanged; + /// UI 值变更回调委托 — UI 组件订阅此回调以更新显示(附带验证状态) + public Action OnUIDataChanged; - /// 通知 UI 更新值 - public void NotifyUIUpdate(object value) + /// 通知 UI 更新值(带验证状态) + public void NotifyUIUpdate(object value, ValidationState state) { - OnUIDataChanged?.Invoke(value); + OnUIDataChanged?.Invoke(value, state); } /// 清除所有 UI 变更订阅 public void ClearUIEvent() { OnUIDataChanged = null; + OnValidationStateChanged = null; } // ============ 便捷方法 ============ diff --git a/Runtime/InterfaceAttributeReflectionGenerator/Scripts/AvrgMvvmBindingCollection.cs b/Runtime/InterfaceAttributeReflectionGenerator/Scripts/AvrgMvvmBindingCollection.cs index 39a5a0d..5f6f265 100644 --- a/Runtime/InterfaceAttributeReflectionGenerator/Scripts/AvrgMvvmBindingCollection.cs +++ b/Runtime/InterfaceAttributeReflectionGenerator/Scripts/AvrgMvvmBindingCollection.cs @@ -4,12 +4,13 @@ using System.Collections.Generic; namespace XericUI.ReflectionUIGenerator { /// - /// 绑定集合 — 统一管理所有 AvrgMvvmBinding + /// 绑定集合 — 统一管理所有 AvrgMvvmBinding,提供字符串索引器和前缀树快速查找 /// public class AvrgMvvmBindingCollection : IEnumerable { private List _bindings = new List(); private AvrgMvvmScheduler _scheduler; + private AvrgBindingTrie _trie; /// /// 绑定的元数据集引用(可选) @@ -21,6 +22,11 @@ namespace XericUI.ReflectionUIGenerator /// public int Count => _bindings.Count; + /// + /// 调度器引用(在 BuildMDVM 时设置) + /// + public AvrgMvvmScheduler Scheduler => _scheduler; + /// /// 添加绑定 /// @@ -29,16 +35,53 @@ namespace XericUI.ReflectionUIGenerator _bindings.Add(binding); } + /// + /// 设置前缀树索引(由 BuildMDVM 构建后注入) + /// + internal void SetTrie(AvrgBindingTrie trie) + { + _trie = trie; + } + /// /// 获取指定索引的绑定 /// public AvrgMvvmBinding this[int index] => _bindings[index]; /// - /// 获取指定成员名称的绑定 + /// 通过成员名访问绑定(如 bindingCollection["speed"]) + /// get: 从数据源读取当前值 + /// set: 走完整的 MVVM 管线:WriteFromData → MarkDataDirty → Flush → UI 同步 + /// + public object this[string memberName] + { + get + { + var binding = FindByMemberName(memberName); + return binding?.ReadFromDataSource(); + } + set + { + var binding = FindByMemberName(memberName); + if (binding != null && _scheduler != null) + { + binding.WriteFromData(value); + _scheduler.MarkDataDirty(binding); + _scheduler.Flush(); + } + } + } + + /// + /// 获取指定成员名称的绑定(优先查 Trie,回退线性查找) /// public AvrgMvvmBinding FindByMemberName(string memberName) { + if (_trie != null) + { + var found = _trie.Find(memberName) ?? _trie.FindByMemberName(memberName); + if (found != null) return found; + } return _bindings.Find(b => b.MetaData?.MemberName == memberName); } @@ -72,6 +115,7 @@ namespace XericUI.ReflectionUIGenerator public void Clear() { _bindings.Clear(); + _trie?.Clear(); _scheduler?.Clear(); } diff --git a/Runtime/InterfaceAttributeReflectionGenerator/Scripts/AvrgMvvmScheduler.cs b/Runtime/InterfaceAttributeReflectionGenerator/Scripts/AvrgMvvmScheduler.cs index 5f1486d..8884c9b 100644 --- a/Runtime/InterfaceAttributeReflectionGenerator/Scripts/AvrgMvvmScheduler.cs +++ b/Runtime/InterfaceAttributeReflectionGenerator/Scripts/AvrgMvvmScheduler.cs @@ -9,8 +9,11 @@ namespace XericUI.ReflectionUIGenerator /// /// 冲突解决策略: /// - 仅数据脏 → 通知 UI 更新 - /// - 仅 UI 脏 → 写入数据源 + /// - 仅 UI 脏 → 写入数据源(写入前校验,Invalid 驳回) /// - 双方都脏 → 走配置的冲突解决策略 + /// + /// 验证拦截:在 CommitToDataSource 前通过 Binding.ValidateValue() 校验。 + /// Invalid 值被驳回不写入,仅通知 UI 进入无效状态。 /// public class AvrgMvvmScheduler { @@ -79,20 +82,36 @@ namespace XericUI.ReflectionUIGenerator if (uiDirty && dataDirty) { - // 双方都脏 — 走冲突解决 + // 双方都脏 — 走冲突解决(内部含验证) ResolveConflict(binding); } else if (dataDirty) { - // 仅数据侧更新 → 通知 UI + // 仅数据侧更新 → 验证后通知 UI var value = binding.DataCachedValue ?? binding.ReadFromDataSource(); + var state = binding.ValidateValue(value); + binding.SetValidationState(state); + binding.CommitToDataSource(value); - binding.NotifyUIUpdate(value); + binding.NotifyUIUpdate(value, state); } else if (uiDirty) { - // 仅 UI 侧更新 → 写入数据源 - binding.CommitToDataSource(binding.UICachedValue); + // 仅 UI 侧更新 → 验证后写入数据源 + var value = binding.UICachedValue; + var state = binding.ValidateValue(value); + + if (state == ValidationState.Invalid) + { + // ★ 无效值:驳回不写入,仅通知 UI 进入无效态 + binding.SetValidationState(state); + binding.NotifyUIUpdate(value, state); + return; + } + + binding.SetValidationState(state); + binding.CommitToDataSource(value); + // UI 是发起方,不再通知自身(但验证状态变更需通知) } } @@ -106,8 +125,10 @@ namespace XericUI.ReflectionUIGenerator var resolvedValue = OnConflictDetected.Invoke(binding, DefaultStrategy); if (resolvedValue != null) { + var state = binding.ValidateValue(resolvedValue); + binding.SetValidationState(state); binding.CommitToDataSource(resolvedValue); - binding.NotifyUIUpdate(resolvedValue); + binding.NotifyUIUpdate(resolvedValue, state); return; } } @@ -120,8 +141,10 @@ namespace XericUI.ReflectionUIGenerator // 2. 检查 ResolverConfig 中的自定义策略 if (_resolverConfig != null && _resolverConfig.TryResolve(binding, out object configResolvedValue)) { + var state = binding.ValidateValue(configResolvedValue); + binding.SetValidationState(state); binding.CommitToDataSource(configResolvedValue); - binding.NotifyUIUpdate(configResolvedValue); + binding.NotifyUIUpdate(configResolvedValue, state); return; } @@ -129,38 +152,70 @@ namespace XericUI.ReflectionUIGenerator switch (DefaultStrategy) { case ConflictResolutionStrategy.PreferUI: - binding.CommitToDataSource(binding.UICachedValue); - binding.NotifyUIUpdate(binding.UICachedValue); - break; - - case ConflictResolutionStrategy.PreferData: - var dataValue = binding.DataCachedValue ?? binding.ReadFromDataSource(); - binding.CommitToDataSource(dataValue); - binding.NotifyUIUpdate(dataValue); - break; - - case ConflictResolutionStrategy.CompareValues: - // 比较UI值和数据值 - object uiVal = binding.UICachedValue; - object dataVal = binding.DataCachedValue ?? binding.ReadFromDataSource(); - - if (Equals(uiVal, dataVal)) { - // 值相同,忽略冲突 - binding.NotifyUIUpdate(dataVal); + var val = binding.UICachedValue; + var state = binding.ValidateValue(val); + if (state == ValidationState.Invalid) + { + binding.SetValidationState(state); + binding.NotifyUIUpdate(val, state); + return; + } + binding.SetValidationState(state); + binding.CommitToDataSource(val); + binding.NotifyUIUpdate(val, state); break; } - // 值不同,回退到 PreferUI - binding.CommitToDataSource(binding.UICachedValue); - binding.NotifyUIUpdate(binding.UICachedValue); - Debug.LogWarning($"[AvrgMvvmScheduler] 值冲突(CompareValues回退PreferUI): {binding.MetaData?.MemberName}"); - break; + case ConflictResolutionStrategy.PreferData: + { + var dataValue = binding.DataCachedValue ?? binding.ReadFromDataSource(); + var state = binding.ValidateValue(dataValue); + binding.SetValidationState(state); + binding.CommitToDataSource(dataValue); + binding.NotifyUIUpdate(dataValue, state); + break; + } + + case ConflictResolutionStrategy.CompareValues: + { + object uiVal = binding.UICachedValue; + object dataVal = binding.DataCachedValue ?? binding.ReadFromDataSource(); + + if (Equals(uiVal, dataVal)) + { + // 值相同,忽略冲突 + var state2 = binding.ValidateValue(dataVal); + binding.SetValidationState(state2); + binding.NotifyUIUpdate(dataVal, state2); + break; + } + + // 值不同,回退到 PreferUI + var val = binding.UICachedValue; + var state = binding.ValidateValue(val); + if (state == ValidationState.Invalid) + { + binding.SetValidationState(state); + binding.NotifyUIUpdate(val, state); + return; + } + binding.SetValidationState(state); + binding.CommitToDataSource(val); + binding.NotifyUIUpdate(val, state); + Debug.LogWarning($"[AvrgMvvmScheduler] 值冲突(CompareValues回退PreferUI): {binding.MetaData?.MemberName}"); + break; + } default: - binding.CommitToDataSource(binding.UICachedValue); - binding.NotifyUIUpdate(binding.UICachedValue); - break; + { + var val = binding.UICachedValue; + var state = binding.ValidateValue(val); + binding.SetValidationState(state); + binding.CommitToDataSource(val); + binding.NotifyUIUpdate(val, state); + break; + } } } diff --git a/Runtime/InterfaceAttributeReflectionGenerator/Scripts/AvrgSubPropertyAccessor.cs b/Runtime/InterfaceAttributeReflectionGenerator/Scripts/AvrgSubPropertyAccessor.cs new file mode 100644 index 0000000..10312fe --- /dev/null +++ b/Runtime/InterfaceAttributeReflectionGenerator/Scripts/AvrgSubPropertyAccessor.cs @@ -0,0 +1,79 @@ +using System; +using System.Reflection; + +namespace XericUI.ReflectionUIGenerator +{ + /// + /// 子属性值访问器 — 通过父访问器 + PropertyPath 访问结构的子字段 + /// + /// 用于拆分属性([XuiavrgSplit])场景,如 Vector3 的 x/y/z 分量。 + /// GetValue 通过反射读取父值的子字段/属性;SetValue 由绑定层的 WriteAction 处理。 + /// + internal class AvrgSubPropertyAccessor : IAvrgValueAccessor + { + private readonly IAvrgValueAccessor _parentAccessor; + private readonly string _propertyPath; + private readonly AvrgMemberMetaData _metaData; + + public AvrgSubPropertyAccessor( + IAvrgValueAccessor parentAccessor, + string propertyPath, + AvrgMemberMetaData metaData) + { + _parentAccessor = parentAccessor ?? throw new ArgumentNullException(nameof(parentAccessor)); + _propertyPath = propertyPath ?? throw new ArgumentNullException(nameof(propertyPath)); + _metaData = metaData; + } + + public Type ValueType => _metaData?.ValueType ?? typeof(float); + + public bool IsReadOnly => _parentAccessor.IsReadOnly; + + public object GetValue() + { + var parentValue = _parentAccessor.GetValue(); + if (parentValue == null || string.IsNullOrEmpty(_propertyPath)) + return null; + + var type = parentValue.GetType(); + var field = type.GetRuntimeField(_propertyPath); + if (field != null) return field.GetValue(parentValue); + + var prop = type.GetRuntimeProperty(_propertyPath); + return prop?.GetValue(parentValue); + } + + /// + /// 子属性设置由绑定层的 WriteAction 统一处理(ReadSubProperty/WriteSubProperty), + /// 此方法不应被直接调用。 + /// + public void SetValue(object value) + { + var parentValue = _parentAccessor.GetValue(); + if (parentValue == null) return; + + var type = parentValue.GetType(); + var field = type.GetRuntimeField(_propertyPath); + if (field != null) + { + field.SetValue(parentValue, value); + _parentAccessor.SetValue(parentValue); + return; + } + + var prop = type.GetRuntimeProperty(_propertyPath); + if (prop != null && prop.CanWrite) + { + prop.SetValue(parentValue, value); + _parentAccessor.SetValue(parentValue); + } + } + + public string FormatDisplay(object value) + { + if (_metaData?.FormatText != null && value is IFormattable formattable) + return formattable.ToString(_metaData.FormatText, null); + return value?.ToString() ?? ""; + } + } +} diff --git a/Runtime/InterfaceAttributeReflectionGenerator/Scripts/ValidationState.cs b/Runtime/InterfaceAttributeReflectionGenerator/Scripts/ValidationState.cs new file mode 100644 index 0000000..722bdd1 --- /dev/null +++ b/Runtime/InterfaceAttributeReflectionGenerator/Scripts/ValidationState.cs @@ -0,0 +1,13 @@ +namespace XericUI.ReflectionUIGenerator +{ + /// + /// 值校验状态枚举 + /// Normal = 正常(通过校验),Warning = 警告(值在警告范围但放行),Invalid = 无效(值被驳回不写入) + /// + public enum ValidationState + { + Normal = 0, + Warning = 1, + Invalid = 2 + } +} diff --git a/Runtime/InterfaceAttributeReflectionGenerator/Scripts/XuiFieldTempEntry.cs b/Runtime/InterfaceAttributeReflectionGenerator/Scripts/XuiFieldTempEntry.cs index e658f02..6be003c 100644 --- a/Runtime/InterfaceAttributeReflectionGenerator/Scripts/XuiFieldTempEntry.cs +++ b/Runtime/InterfaceAttributeReflectionGenerator/Scripts/XuiFieldTempEntry.cs @@ -308,14 +308,14 @@ namespace XericUI.ReflectionUIGenerator foreach (var binding in _bindings) { var value = binding.ReadFromDataSource(); - binding.NotifyUIUpdate(value); + binding.NotifyUIUpdate(value, ValidationState.Normal); if (binding.SubBindings?.Count > 0) { foreach (var subBinding in binding.SubBindings) { var subValue = subBinding.ReadFromDataSource(); - subBinding.NotifyUIUpdate(subValue); + subBinding.NotifyUIUpdate(subValue, ValidationState.Normal); } } } diff --git a/Runtime/InterfaceAttributeReflectionGenerator/Scripts/XuiavrgInspectorManager.cs b/Runtime/InterfaceAttributeReflectionGenerator/Scripts/XuiavrgInspectorManager.cs index 6aae0e9..d2d81a2 100644 --- a/Runtime/InterfaceAttributeReflectionGenerator/Scripts/XuiavrgInspectorManager.cs +++ b/Runtime/InterfaceAttributeReflectionGenerator/Scripts/XuiavrgInspectorManager.cs @@ -127,16 +127,17 @@ namespace XericUI.ReflectionUIGenerator if (bindingCollection != null) { - var matchedBindings = bindingCollection.FindByMemberName(metaData.MemberName); - if (matchedBindings != null) + var scheduler = bindingCollection.Scheduler; + var matchedBinding = bindingCollection.FindByMemberName(metaData.MemberName); + if (matchedBinding != null) { - entry.AddBinding(matchedBindings, null); + entry.AddBinding(matchedBinding, scheduler); } else { foreach (var binding in bindingCollection) { - entry.AddBinding(binding, null); + entry.AddBinding(binding, scheduler); break; } } @@ -156,13 +157,14 @@ namespace XericUI.ReflectionUIGenerator if (CurrentEntries != null && bindingCollection != null) { + var scheduler = bindingCollection.Scheduler; foreach (var entry in CurrentEntries) { if (entry is XuiFieldTempEntry tempEntry) { var matched = bindingCollection.FindByMemberName(entry.EntryName); if (matched != null) - tempEntry.AddBinding(matched, null); + tempEntry.AddBinding(matched, scheduler); } } } @@ -457,5 +459,7 @@ namespace XericUI.ReflectionUIGenerator _leafGroupMap.Clear(); } + + #endregion } } \ No newline at end of file diff --git a/Runtime/OverrideUI/AvrgValues/XUIAvrgMDColorValue.cs b/Runtime/OverrideUI/AvrgValues/XUIAvrgMDColorValue.cs index 9790e8a..d857e9c 100644 --- a/Runtime/OverrideUI/AvrgValues/XUIAvrgMDColorValue.cs +++ b/Runtime/OverrideUI/AvrgValues/XUIAvrgMDColorValue.cs @@ -4,20 +4,17 @@ using UnityEngine.UI; namespace XericUI.ReflectionUIGenerator { /// - /// 颜色值组件 — 处理 Color 类型的显示 - /// 通过 Image.color 展示当前颜色值 - /// 颜色修改可通过 RGB 子节点 InputField 或额外的颜色选择逻辑完成 + /// Color 值组件 — 通过 Image 显示颜色,可选点击弹出颜色选择器 /// [AddComponentMenu("Xeric Library/UI Action Vessel/Values/XUIAvrgMDColorValue")] public class XUIAvrgMDColorValue : XUIAvrgMDValue { - [SerializeField] - [Tooltip("用于展示颜色的 Image 组件(未设置则自动查找)")] - private Image _colorDisplay; + [SerializeField, Tooltip("关联的 Image 组件(未设置则自动查找)")] + private Image _colorImage; private void Awake() { - if (_colorDisplay == null) _colorDisplay = GetComponent(); + if (_colorImage == null) _colorImage = GetComponent(); } public override void BindToAccessor( @@ -30,29 +27,23 @@ namespace XericUI.ReflectionUIGenerator if (binding == null || accessor == null) return; - // 数据 → UI - binding.OnUIDataChanged += value => + binding.OnUIDataChanged += (value, state) => { - if (value is Color color && _colorDisplay != null) - { - _colorDisplay.color = color; - } + UpdateValueDisplay(value, state); }; + binding.OnValidationStateChanged += OnValidationStateChanged; - // 首次同步 var initialValue = accessor.GetValue(); - if (initialValue is Color initColor && _colorDisplay != null) - { - _colorDisplay.color = initColor; - } + UpdateValueDisplay(initialValue, ValidationState.Normal); } - protected override void UpdateDisplayValue(object value) + protected override void UpdateValueDisplay(object value, ValidationState state) { - if (value is Color color && _colorDisplay != null) - { - _colorDisplay.color = color; - } + // 值显示:刷新 Image.color + if (_colorImage != null && value is Color c) + _colorImage.color = c; + + base.UpdateValueDisplay(value, state); } } } diff --git a/Runtime/OverrideUI/AvrgValues/XUIAvrgMDDropdownValue.cs b/Runtime/OverrideUI/AvrgValues/XUIAvrgMDDropdownValue.cs index a62e344..20dbdb9 100644 --- a/Runtime/OverrideUI/AvrgValues/XUIAvrgMDDropdownValue.cs +++ b/Runtime/OverrideUI/AvrgValues/XUIAvrgMDDropdownValue.cs @@ -1,7 +1,7 @@ using System.Collections.Generic; +using System; using System.Linq; using UnityEngine; -using UnityEngine.UI; namespace XericUI.ReflectionUIGenerator { @@ -20,6 +20,8 @@ namespace XericUI.ReflectionUIGenerator private Dropdown _dropdown; #endif + private List _options; + private void Awake() { #if dUI_TextMeshPro @@ -29,6 +31,24 @@ namespace XericUI.ReflectionUIGenerator #endif } + // ============ 对象池生命周期 ============ + + public override void OnPoolGet(AvrgMemberMetaData metaData) + { + base.OnPoolGet(metaData); + _options = BuildOptions(metaData?.ValueType); + } + + public override void OnPoolRelease() + { + if (_dropdown != null) + _dropdown.onValueChanged.RemoveAllListeners(); + _options = null; + base.OnPoolRelease(); + } + + // ============ 核心绑定 ============ + public override void BindToAccessor( IAvrgValueAccessor accessor, AvrgMvvmBinding binding, @@ -39,66 +59,71 @@ namespace XericUI.ReflectionUIGenerator if (binding == null || accessor == null || _dropdown == null) return; - // 构建选项列表 - var options = BuildOptions(accessor.ValueType); + // 填充选项 _dropdown.ClearOptions(); - _dropdown.AddOptions(options); + _dropdown.AddOptions(_options); // UI → 数据 _dropdown.onValueChanged.AddListener(index => { - if (index >= 0 && index < options.Count) + if (index >= 0 && index < _options.Count) { - object convertedVal = ConvertOptionToValue(options[index], accessor.ValueType); + object convertedVal = ConvertOptionToValue(_options[index], accessor.ValueType); binding.WriteFromUI(convertedVal); scheduler?.MarkUIDirty(binding); } }); - // 数据 → UI - binding.OnUIDataChanged += value => + // 数据 → UI(带验证状态) + binding.OnUIDataChanged += (value, state) => { - int idx = FindOptionIndex(value, options); + int idx = FindOptionIndex(value); if (idx >= 0) _dropdown.SetValueWithoutNotify(idx); + + base.UpdateValueDisplay(value, state); }; + // 验证状态变更 + binding.OnValidationStateChanged += OnValidationStateChanged; + // 首次同步 var initialValue = accessor.GetValue(); - int initialIdx = FindOptionIndex(initialValue, options); + int initialIdx = FindOptionIndex(initialValue); if (initialIdx >= 0) _dropdown.SetValueWithoutNotify(initialIdx); + UpdateValueDisplay(initialValue, ValidationState.Normal); } - private List BuildOptions(System.Type valueType) + private List BuildOptions(Type valueType) { if (BoundMetaData?.PresetOptions != null && BoundMetaData.PresetOptions.Count > 0) return BoundMetaData.PresetOptions.ToList(); if (valueType != null && valueType.IsEnum) - return System.Enum.GetNames(valueType).ToList(); + return Enum.GetNames(valueType).ToList(); return new List(); } - private int FindOptionIndex(object value, List options) + private int FindOptionIndex(object value) { - if (value == null || options.Count == 0) return -1; + if (value == null || _options == null || _options.Count == 0) return -1; string valueStr = value.ToString(); - for (int i = 0; i < options.Count; i++) + for (int i = 0; i < _options.Count; i++) { - if (options[i] == valueStr) return i; + if (_options[i] == valueStr) return i; } return -1; } - private static object ConvertOptionToValue(string option, System.Type targetType) + private static object ConvertOptionToValue(string option, Type targetType) { if (targetType == typeof(string)) return option; if (targetType == typeof(int) && int.TryParse(option, out int intVal)) return intVal; if (targetType != null && targetType.IsEnum) - return System.Enum.Parse(targetType, option); + return Enum.Parse(targetType, option); return option; } } diff --git a/Runtime/OverrideUI/AvrgValues/XUIAvrgMDInputFieldValue.cs b/Runtime/OverrideUI/AvrgValues/XUIAvrgMDInputFieldValue.cs index 32a7e9d..5cf2ef4 100644 --- a/Runtime/OverrideUI/AvrgValues/XUIAvrgMDInputFieldValue.cs +++ b/Runtime/OverrideUI/AvrgValues/XUIAvrgMDInputFieldValue.cs @@ -1,12 +1,11 @@ using System; using UnityEngine; using UnityEngine.UI; -using XericLibrary.Runtime.SuperStyleSheet; namespace XericUI.ReflectionUIGenerator { /// - /// InputField 值组件 — 处理 string / float / int 类型的文本输入交互 + /// InputField 值组件 — 处理 string / float / int / double 类型的文本输入交互 /// [AddComponentMenu("Xeric Library/UI Action Vessel/Values/XUIAvrgMDInputFieldValue")] public class XUIAvrgMDInputFieldValue : XUIAvrgMDValue @@ -26,10 +25,7 @@ namespace XericUI.ReflectionUIGenerator [SerializeField, Tooltip("减少按钮(点击按步进值 -)")] private Button _decrementButton; - // ============ 样式表参数 ============ - private readonly StyleMember _styleStepValue = new() { StylePath = "xuiavrg/input/stepValue" }; - - // ============ 生命周期 ============ + private const float DefaultStep = 1f; private void Awake() { @@ -40,16 +36,18 @@ namespace XericUI.ReflectionUIGenerator #endif } - protected override void OnEnable() - { - base.OnEnable(); - _styleStepValue.Register(); - } + // ============ 对象池生命周期 ============ - protected override void OnDisable() + public override void OnPoolRelease() { - base.OnDisable(); - _styleStepValue.Unregister(); + if (_incrementButton != null) + _incrementButton.onClick.RemoveAllListeners(); + if (_decrementButton != null) + _decrementButton.onClick.RemoveAllListeners(); + if (_inputField != null) + _inputField.onEndEdit.RemoveAllListeners(); + + base.OnPoolRelease(); } // ============ 核心绑定 ============ @@ -64,6 +62,7 @@ namespace XericUI.ReflectionUIGenerator if (binding == null || accessor == null || _inputField == null) return; + // 根据类型设置输入模式 #if dUI_TextMeshPro if (accessor.ValueType == typeof(float) || accessor.ValueType == typeof(int) || accessor.ValueType == typeof(double)) @@ -92,33 +91,29 @@ namespace XericUI.ReflectionUIGenerator scheduler?.MarkUIDirty(binding); }); - // 数据 → UI - binding.OnUIDataChanged += value => + // 数据 → UI(带验证状态) + binding.OnUIDataChanged += (value, state) => { _inputField.SetTextWithoutNotify(accessor.FormatDisplay(value)); + base.UpdateValueDisplay(value, state); }; + // 验证状态变更 + binding.OnValidationStateChanged += OnValidationStateChanged; + // 首次同步 var initialValue = accessor.GetValue(); _inputField.SetTextWithoutNotify(accessor.FormatDisplay(initialValue)); - - RefreshCommonNodes(); + UpdateValueDisplay(initialValue, ValidationState.Normal); } // ============ 步进 ============ - private float GetStepValue() - { - var val = GetStyleValue(_styleStepValue); - return val?.GetValueAsFloat() ?? 1f; - } - private void OnIncrementClicked() { if (CurrentAccessor?.IsReadOnly == true || _inputField == null) return; if (!float.TryParse(_inputField.text, out float current)) return; - float step = GetStepValue(); - float newVal = current + step; + float newVal = current + DefaultStep; _inputField.text = CurrentAccessor.FormatDisplay( ConvertValueForAccessor(newVal, CurrentAccessor.ValueType)); } @@ -127,12 +122,32 @@ namespace XericUI.ReflectionUIGenerator { if (CurrentAccessor?.IsReadOnly == true || _inputField == null) return; if (!float.TryParse(_inputField.text, out float current)) return; - float step = GetStepValue(); - float newVal = current - step; + float newVal = current - DefaultStep; _inputField.text = CurrentAccessor.FormatDisplay( ConvertValueForAccessor(newVal, CurrentAccessor.ValueType)); } + // ============ 验证状态回调 ============ + + protected override void OnValidationStateChanged(ValidationState state) + { + base.OnValidationStateChanged(state); + + // InputField 文字颜色随验证状态变化(通过 Text 组件) + if (_inputField != null) + { + var textComponent = _inputField.textComponent; + if (textComponent != null) + { + textComponent.color = state == ValidationState.Invalid + ? Color.red + : state == ValidationState.Warning + ? new Color(1f, 0.92f, 0.016f) + : Color.white; + } + } + } + private static object ConvertValueForAccessor(float val, Type targetType) { if (targetType == typeof(int)) return (int)val; diff --git a/Runtime/OverrideUI/AvrgValues/XUIAvrgMDLabelValue.cs b/Runtime/OverrideUI/AvrgValues/XUIAvrgMDLabelValue.cs index b161c89..85c63b7 100644 --- a/Runtime/OverrideUI/AvrgValues/XUIAvrgMDLabelValue.cs +++ b/Runtime/OverrideUI/AvrgValues/XUIAvrgMDLabelValue.cs @@ -1,32 +1,19 @@ using UnityEngine; +using UnityEngine.UI; namespace XericUI.ReflectionUIGenerator { /// - /// 标签值组件 — 处理只读文本的纯展示 - /// 适用于 Label / Context / Unit / ReadOnly 等展示型 Tag + /// Label 只读值组件 — 纯文本展示,无交互控件 /// [AddComponentMenu("Xeric Library/UI Action Vessel/Values/XUIAvrgMDLabelValue")] public class XUIAvrgMDLabelValue : XUIAvrgMDValue { - [SerializeField] - [Tooltip("关联的 Text 组件(未设置则自动查找)")] - private UnityEngine.UI.Text _uiText; - #if dUI_TextMeshPro - [SerializeField] - [Tooltip("关联的 TMP_Text 组件(未设置则自动查找)")] + [SerializeField, Tooltip("关联的 TMP_Text 组件(标签文本 + 值文本共用一个 Text 时使用)")] private TMPro.TMP_Text _tmpText; #endif - private void Awake() - { - if (_uiText == null) _uiText = GetComponent(); -#if dUI_TextMeshPro - if (_tmpText == null) _tmpText = GetComponent(); -#endif - } - public override void BindToAccessor( IAvrgValueAccessor accessor, AvrgMvvmBinding binding, @@ -37,31 +24,14 @@ namespace XericUI.ReflectionUIGenerator if (binding == null || accessor == null) return; - // 数据 → UI - binding.OnUIDataChanged += value => + binding.OnUIDataChanged += (value, state) => { - SetText(value); + UpdateValueDisplay(value, state); }; + binding.OnValidationStateChanged += OnValidationStateChanged; - // 首次同步 var initialValue = accessor.GetValue(); - SetText(initialValue); - } - - private void SetText(object value) - { - string displayStr = value?.ToString() ?? ""; - -#if dUI_TextMeshPro - if (_tmpText != null) - { - _tmpText.text = displayStr; - return; - } -#endif - - if (_uiText != null) - _uiText.text = displayStr; + UpdateValueDisplay(initialValue, ValidationState.Normal); } } } diff --git a/Runtime/OverrideUI/AvrgValues/XUIAvrgMDSliderValue.cs b/Runtime/OverrideUI/AvrgValues/XUIAvrgMDSliderValue.cs index f5d2cfb..5f3f6d5 100644 --- a/Runtime/OverrideUI/AvrgValues/XUIAvrgMDSliderValue.cs +++ b/Runtime/OverrideUI/AvrgValues/XUIAvrgMDSliderValue.cs @@ -1,19 +1,17 @@ using System; using UnityEngine; using UnityEngine.UI; -using XericLibrary.Runtime.SuperStyleSheet; namespace XericUI.ReflectionUIGenerator { /// /// Slider 值组件 — 处理 float / int / double 类型的滑块交互 - /// 适用于 Shader Range 属性或标记了 [XuiavrgFieldTag(Slider)] 的字段 + /// 适用 Shader Range 属性或标记了 [XuiavrgFieldTag(Slider)] 的字段 /// [AddComponentMenu("Xeric Library/UI Action Vessel/Values/XUIAvrgMDSliderValue")] public class XUIAvrgMDSliderValue : XUIAvrgMDValue { - [SerializeField] - [Tooltip("关联的 Slider 组件(未设置则自动查找)")] + [SerializeField, Tooltip("关联的 Slider 组件(未设置则自动查找)")] private Slider _slider; [Header("Slider 专属插槽")] @@ -24,37 +22,44 @@ namespace XericUI.ReflectionUIGenerator private Button _decrementButton; #if dUI_TextMeshPro - [SerializeField] - [Tooltip("值显示标签(可选,未设置则不更新)")] + [SerializeField, Tooltip("值显示标签(可选,未设置则不更新)")] private TMPro.TMP_Text _valueLabel; #endif - // ============ 样式表参数 ============ - private readonly StyleMember _styleDefaultStep = new() { StylePath = "xuiavrg/slider/defaultStep" }; - private readonly StyleMember _styleMinStep = new() { StylePath = "xuiavrg/slider/minStep" }; - private readonly StyleMember _styleMaxStep = new() { StylePath = "xuiavrg/slider/maxStep" }; - - // ============ 生命周期 ============ + private const float DefaultStep = 0.1f; private void Awake() { if (_slider == null) _slider = GetComponent(); } - protected override void OnEnable() + // ============ 对象池生命周期 ============ + + public override void OnPoolGet(AvrgMemberMetaData metaData) { - base.OnEnable(); - _styleDefaultStep.Register(); - _styleMinStep.Register(); - _styleMaxStep.Register(); + base.OnPoolGet(metaData); + + if (_slider == null) return; + + // 从元数据初始化 Slider 参数 + if (metaData?.RangeMin.HasValue == true) + _slider.minValue = metaData.RangeMin.Value; + if (metaData?.RangeMax.HasValue == true) + _slider.maxValue = metaData.RangeMax.Value; + _slider.wholeNumbers = metaData?.ValueType == typeof(int); } - protected override void OnDisable() + public override void OnPoolRelease() { - base.OnDisable(); - _styleDefaultStep.Unregister(); - _styleMinStep.Unregister(); - _styleMaxStep.Unregister(); + // 清除按钮监听 + if (_incrementButton != null) + _incrementButton.onClick.RemoveAllListeners(); + if (_decrementButton != null) + _decrementButton.onClick.RemoveAllListeners(); + if (_slider != null) + _slider.onValueChanged.RemoveAllListeners(); + + base.OnPoolRelease(); } // ============ 核心绑定 ============ @@ -69,17 +74,6 @@ namespace XericUI.ReflectionUIGenerator if (binding == null || accessor == null || _slider == null) return; - // 设置 Slider 参数 - _slider.wholeNumbers = accessor.ValueType == typeof(int); - - if (BoundMetaData != null) - { - if (BoundMetaData.RangeMin.HasValue) - _slider.minValue = BoundMetaData.RangeMin.Value; - if (BoundMetaData.RangeMax.HasValue) - _slider.maxValue = BoundMetaData.RangeMax.Value; - } - // 增减按钮 if (_incrementButton != null) _incrementButton.onClick.AddListener(OnIncrementClicked); @@ -99,8 +93,8 @@ namespace XericUI.ReflectionUIGenerator #endif }); - // 数据 → UI - binding.OnUIDataChanged += value => + // 数据 → UI(带验证状态) + binding.OnUIDataChanged += (value, state) => { float sliderVal = Convert.ToSingle(value); _slider.SetValueWithoutNotify(sliderVal); @@ -109,8 +103,14 @@ namespace XericUI.ReflectionUIGenerator if (_valueLabel != null) _valueLabel.text = accessor.FormatDisplay(value); #endif + + // 委托基类处理标签合成 + 值文本 + base.UpdateValueDisplay(value, state); }; + // 验证状态变更 + binding.OnValidationStateChanged += OnValidationStateChanged; + // 首次同步 var initialValue = accessor.GetValue(); if (initialValue != null) @@ -121,23 +121,14 @@ namespace XericUI.ReflectionUIGenerator _valueLabel.text = accessor.FormatDisplay(initialValue); #endif } - - RefreshCommonNodes(); + UpdateValueDisplay(initialValue ?? 0f, ValidationState.Normal); } // ============ 步进值 ============ private float GetStepValue() { - var val = GetStyleValue(_styleDefaultStep); - if (val != null) - { - float step = val.GetValueAsFloat(); - float min = GetStyleValue(_styleMinStep)?.GetValueAsFloat() ?? 0.001f; - float max = GetStyleValue(_styleMaxStep)?.GetValueAsFloat() ?? 1000f; - return Mathf.Clamp(step, min, max); - } - return _slider.wholeNumbers ? 1f : 0.1f; + return _slider.wholeNumbers ? 1f : DefaultStep; } private void OnIncrementClicked() @@ -158,6 +149,28 @@ namespace XericUI.ReflectionUIGenerator _slider.value = newVal; } + // ============ 验证状态回调 ============ + + protected override void OnValidationStateChanged(ValidationState state) + { + base.OnValidationStateChanged(state); + + // Slider 填充色随验证状态变化 + if (_slider != null) + { + var fillArea = _slider.fillRect; + if (fillArea != null && fillArea.TryGetComponent(out var fill)) + { + fill.color = state switch + { + ValidationState.Invalid => Color.red, + ValidationState.Warning => new Color(1f, 0.92f, 0.016f), + _ => Color.white + }; + } + } + } + private static object ConvertValueForAccessor(float val, Type targetType) { if (targetType == typeof(int)) return (int)val; diff --git a/Runtime/OverrideUI/AvrgValues/XUIAvrgMDTextureValue.cs b/Runtime/OverrideUI/AvrgValues/XUIAvrgMDTextureValue.cs index 35e109f..ed4c08e 100644 --- a/Runtime/OverrideUI/AvrgValues/XUIAvrgMDTextureValue.cs +++ b/Runtime/OverrideUI/AvrgValues/XUIAvrgMDTextureValue.cs @@ -4,25 +4,22 @@ using UnityEngine.UI; namespace XericUI.ReflectionUIGenerator { /// - /// 纹理值组件 — 处理 Texture 类型的预览显示 - /// 通过 RawImage.texture 展示当前纹理 + /// Texture 值组件 — 通过 RawImage 显示纹理 /// [AddComponentMenu("Xeric Library/UI Action Vessel/Values/XUIAvrgMDTextureValue")] public class XUIAvrgMDTextureValue : XUIAvrgMDValue { - [SerializeField] - [Tooltip("用于展示纹理的 RawImage 组件(未设置则自动查找)")] - private RawImage _textureDisplay; + [SerializeField, Tooltip("关联的 RawImage 组件(未设置则自动查找)")] + private RawImage _rawImage; #if dUI_TextMeshPro - [SerializeField] - [Tooltip("纹理名称标签(可选,用于显示纹理资源名)")] + [SerializeField, Tooltip("纹理名称标签(可选)")] private TMPro.TMP_Text _nameLabel; #endif private void Awake() { - if (_textureDisplay == null) _textureDisplay = GetComponent(); + if (_rawImage == null) _rawImage = GetComponent(); } public override void BindToAccessor( @@ -35,35 +32,29 @@ namespace XericUI.ReflectionUIGenerator if (binding == null || accessor == null) return; - // 数据 → UI - binding.OnUIDataChanged += value => + binding.OnUIDataChanged += (value, state) => { - if (value is Texture tex && _textureDisplay != null) - { - _textureDisplay.texture = tex; - } + UpdateValueDisplay(value, state); + }; + binding.OnValidationStateChanged += OnValidationStateChanged; + var initialValue = accessor.GetValue(); + UpdateValueDisplay(initialValue, ValidationState.Normal); + } + + protected override void UpdateValueDisplay(object value, ValidationState state) + { + // 值显示:刷新 RawImage.texture + if (_rawImage != null && value is Texture tex) + { + _rawImage.texture = tex; #if dUI_TextMeshPro if (_nameLabel != null) - { - _nameLabel.text = value is Texture t && t != null ? t.name : "None"; - } + _nameLabel.text = tex?.name ?? ""; #endif - }; - - // 首次同步 - var initialValue = accessor.GetValue(); - if (initialValue is Texture initTex && _textureDisplay != null) - { - _textureDisplay.texture = initTex; } -#if dUI_TextMeshPro - if (_nameLabel != null) - { - _nameLabel.text = initialValue is Texture t && t != null ? t.name : "None"; - } -#endif + base.UpdateValueDisplay(value, state); } } } diff --git a/Runtime/OverrideUI/AvrgValues/XUIAvrgMDToggleGroupValue.cs b/Runtime/OverrideUI/AvrgValues/XUIAvrgMDToggleGroupValue.cs index 01f00f8..9c4ff8a 100644 --- a/Runtime/OverrideUI/AvrgValues/XUIAvrgMDToggleGroupValue.cs +++ b/Runtime/OverrideUI/AvrgValues/XUIAvrgMDToggleGroupValue.cs @@ -56,8 +56,8 @@ namespace XericUI.ReflectionUIGenerator }); } - // 数据 → UI - binding.OnUIDataChanged += value => + // 数据 → UI(带验证状态) + binding.OnUIDataChanged += (value, state) => { string valueStr = value?.ToString(); for (int i = 0; i < _toggles.Count && i < _optionLabels.Count; i++) @@ -65,11 +65,16 @@ namespace XericUI.ReflectionUIGenerator if (_optionLabels[i] == valueStr) { _toggles[i].SetIsOnWithoutNotify(true); - return; + break; } } + + base.UpdateValueDisplay(value, state); }; + // 验证状态变更 + binding.OnValidationStateChanged += OnValidationStateChanged; + // 首次同步 var initialValue = accessor.GetValue(); string initStr = initialValue?.ToString(); diff --git a/Runtime/OverrideUI/AvrgValues/XUIAvrgMDToggleValue.cs b/Runtime/OverrideUI/AvrgValues/XUIAvrgMDToggleValue.cs index 3d741cf..ee63b89 100644 --- a/Runtime/OverrideUI/AvrgValues/XUIAvrgMDToggleValue.cs +++ b/Runtime/OverrideUI/AvrgValues/XUIAvrgMDToggleValue.cs @@ -1,43 +1,29 @@ using UnityEngine; using UnityEngine.UI; -using XericLibrary.Runtime.SuperStyleSheet; namespace XericUI.ReflectionUIGenerator { /// /// Toggle 值组件 — 处理 bool 类型的开关交互 - /// 支持样式表控制开关颜色 /// [AddComponentMenu("Xeric Library/UI Action Vessel/Values/XUIAvrgMDToggleValue")] public class XUIAvrgMDToggleValue : XUIAvrgMDValue { - [SerializeField] - [Tooltip("关联的 Toggle 组件(未设置则自动查找)")] + [SerializeField, Tooltip("关联的 Toggle 组件(未设置则自动查找)")] private Toggle _toggle; - // ============ 样式表参数 ============ - private readonly StyleMember _styleOnColor = new() { StylePath = "xuiavrg/toggle/onColor" }; - private readonly StyleMember _styleOffColor = new() { StylePath = "xuiavrg/toggle/offColor" }; - - // ============ 生命周期 ============ - private void Awake() { if (_toggle == null) _toggle = GetComponent(); } - protected override void OnEnable() - { - base.OnEnable(); - _styleOnColor.Register(); - _styleOffColor.Register(); - } + // ============ 对象池生命周期 ============ - protected override void OnDisable() + public override void OnPoolRelease() { - base.OnDisable(); - _styleOnColor.Unregister(); - _styleOffColor.Unregister(); + if (_toggle != null) + _toggle.onValueChanged.RemoveAllListeners(); + base.OnPoolRelease(); } // ============ 核心绑定 ============ @@ -60,36 +46,53 @@ namespace XericUI.ReflectionUIGenerator { binding.WriteFromUI(val); scheduler?.MarkUIDirty(binding); - ApplyToggleColor(val); }); - // 数据 → UI - binding.OnUIDataChanged += value => + // 数据 → UI(带验证状态) + binding.OnUIDataChanged += (value, state) => { bool isOn = value is bool b && b; _toggle.SetIsOnWithoutNotify(isOn); - ApplyToggleColor(isOn); + ApplyToggleColor(isOn, state); + + base.UpdateValueDisplay(value, state); }; + // 验证状态变更 + binding.OnValidationStateChanged += OnValidationStateChanged; + // 首次同步 var initialValue = accessor.GetValue(); bool initialOn = initialValue is bool bv && bv; _toggle.SetIsOnWithoutNotify(initialOn); - ApplyToggleColor(initialOn); - - RefreshCommonNodes(); + ApplyToggleColor(initialOn, ValidationState.Normal); + UpdateValueDisplay(initialValue, ValidationState.Normal); } - // ============ 样式颜色 ============ + // ============ 验证状态回调 ============ - private void ApplyToggleColor(bool isOn) + protected override void OnValidationStateChanged(ValidationState state) { - var target = _toggle.targetGraphic; + base.OnValidationStateChanged(state); + + // Switch 验证状态也影响 targetGraphic 颜色 + if (_toggle != null) + { + ApplyToggleColor(_toggle.isOn, state); + } + } + + private void ApplyToggleColor(bool isOn, ValidationState state) + { + var target = _toggle?.targetGraphic; if (target == null) return; - var colorVal = isOn ? GetStyleValue(_styleOnColor) : GetStyleValue(_styleOffColor); - if (colorVal != null) - target.color = colorVal; + if (state == ValidationState.Invalid) + target.color = Color.red; + else if (state == ValidationState.Warning) + target.color = new Color(1f, 0.92f, 0.016f); + else + target.color = isOn ? Color.green : new Color(0.5f, 0.5f, 0.5f, 1f); } } } diff --git a/Runtime/OverrideUI/AvrgValues/XUIAvrgMDValue.cs b/Runtime/OverrideUI/AvrgValues/XUIAvrgMDValue.cs index 6948365..9ddee2a 100644 --- a/Runtime/OverrideUI/AvrgValues/XUIAvrgMDValue.cs +++ b/Runtime/OverrideUI/AvrgValues/XUIAvrgMDValue.cs @@ -1,6 +1,6 @@ using System.Collections.Generic; +using System.Text; using UnityEngine; -using XericLibrary.Runtime.SuperStyleSheet; namespace XericUI.ReflectionUIGenerator { @@ -9,11 +9,11 @@ namespace XericUI.ReflectionUIGenerator /// /// 职责: /// 1. 提供 EntryTag/TagIndex 供 XuiFieldTempEntry 匹配 - /// 2. 持有绑定的元数据引用 - /// 3. 通过 virtual BindToAccessor() 让子类自行管理 UI 交互事件和显示更新 - /// 4. 初始化时广播元数据到所有子节点上的 IXUIAvrgMDValueStyle + /// 2. 持有绑定的元数据引用,管理验证状态 + /// 3. 通过 virtual BindToAccessor() 让子类自行管理 UI 交互事件 + /// 4. 对象池生命周期:OnPoolGet(meta) 初始化、OnPoolRelease() 重置 /// 5. 定义绑定槽(BindingSlots),供拆分属性子绑定匹配 - /// 6. 管理通用 UI 插槽(标签、警告、只读、单位、值显示)+ SuperStyleSheet 样式读取 + /// 6. 通过 UpdateValueDisplay() 合成标签文本(名称 + 只读 + 警告标记) /// [AddComponentMenu("Xeric Library/UI Action Vessel/XUIAvrgMDValue")] public class XUIAvrgMDValue : MonoBehaviour @@ -47,29 +47,29 @@ namespace XericUI.ReflectionUIGenerator [Tooltip("拆分属性子绑定槽列表 — 用于 XuiavrgSplit 的子属性匹配")] private List _bindingSlots = new List(); - // ============ 通用 UI 插槽 ============ + // ============ UI 文本插槽 ============ - [Header("通用 UI 插槽")] - [SerializeField, Tooltip("字段标签节点 — 显示字段名称 + 只读前缀。为 null 则不显示标签行")] - protected GameObject fieldLabelNode; + [Header("UI 文本插槽")] + [SerializeField, Tooltip("字段标签文本 — 显示 \"名称 (只读) ⚠警告\",为 null 则不显示标签")] +#if dUI_TextMeshPro + protected TMPro.TMP_Text _fieldLabelText; +#else + protected UnityEngine.UI.Text _fieldLabelText; +#endif - [SerializeField, Tooltip("警告样式节点 — 验证失败时 SetActive(true),颜色由样式表控制")] - protected GameObject warningNode; + [SerializeField, Tooltip("值显示文本 — 可为 null(Slider/Color/Texture 子类不依赖文本)")] +#if dUI_TextMeshPro + protected TMPro.TMP_Text _valueDisplayText; +#else + protected UnityEngine.UI.Text _valueDisplayText; +#endif - [SerializeField, Tooltip("只读标记节点 — IsReadOnly 时 SetActive(true)")] - protected GameObject readOnlyNode; - - [SerializeField, Tooltip("单位文本节点 — 显示元数据的 UnitLabelName")] - protected GameObject unitTextNode; - - [SerializeField, Tooltip("值显示文本节点 — 可为 null(子类如 Slider/Color 不依赖文本)")] - protected GameObject valueTextNode; - - // ============ 样式表参数(路径硬编码,命名空间从 BoundMetaData.StyleNamespace 获取)============ - - private readonly StyleMember _styleStepValue = new() { StylePath = "xuiavrg/base/stepValue" }; - private readonly StyleMember _styleReadOnlyPrefix = new() { StylePath = "xuiavrg/base/readOnlyPrefix" }; - private readonly StyleMember _styleWarningColor = new() { StylePath = "xuiavrg/base/warningColor" }; + [SerializeField, Tooltip("单位文本 — 可为 null")] +#if dUI_TextMeshPro + protected TMPro.TMP_Text _unitText; +#else + protected UnityEngine.UI.Text _unitText; +#endif // ============ 公共属性 ============ @@ -82,26 +82,44 @@ namespace XericUI.ReflectionUIGenerator /// 当前关联的 MVVM 绑定 public AvrgMvvmBinding CurrentBinding { get; protected set; } + /// 当前验证状态 + public ValidationState CurrentValidationState { get; protected set; } = ValidationState.Normal; + public XuiavrgFieldTag EntryTag => _entryTag; public int TagIndex => _tagIndex; /// 只读访问绑定槽列表 public IReadOnlyList AvailableBindingSlots => _bindingSlots; - // ============ 生命周期 ============ + // ============ 对象池生命周期 ============ - protected virtual void OnEnable() + /// + /// 从对象池取出时调用 — 注入元数据并初始化子属性 + /// 子类可覆写以初始化专属组件参数(Slider.minValue/maxValue 等) + /// + public virtual void OnPoolGet(AvrgMemberMetaData metaData) { - _styleStepValue.Register(); - _styleReadOnlyPrefix.Register(); - _styleWarningColor.Register(); + BoundMetaData = metaData; + CurrentValidationState = ValidationState.Normal; + + // 子类覆写点 } - protected virtual void OnDisable() + /// + /// 回收到对象池时调用 — 重置所有状态到初始值 + /// 子类可覆写以清理专属组件事件 + /// + public virtual void OnPoolRelease() { - _styleStepValue.Unregister(); - _styleReadOnlyPrefix.Unregister(); - _styleWarningColor.Unregister(); + CurrentAccessor = null; + CurrentBinding = null; + BoundMetaData = null; + CurrentValidationState = ValidationState.Normal; + + // 重置 UI 文本 + if (_fieldLabelText != null) _fieldLabelText.text = ""; + if (_valueDisplayText != null) _valueDisplayText.text = ""; + if (_unitText != null) _unitText.text = ""; } // ============ 核心绑定接口 ============ @@ -119,44 +137,107 @@ namespace XericUI.ReflectionUIGenerator if (binding == null || accessor == null) return; - // 默认:数据 → UI 单向同步 - binding.OnUIDataChanged += value => + // 数据 → UI(带验证状态) + binding.OnUIDataChanged += (value, state) => { - UpdateDisplayValue(value); + UpdateValueDisplay(value, state); }; + // 验证状态变更 + binding.OnValidationStateChanged += OnValidationStateChanged; + // 首次同步 var initialValue = accessor.GetValue(); - UpdateDisplayValue(initialValue); + UpdateValueDisplay(initialValue, ValidationState.Normal); + } - // 刷新通用插槽 - RefreshCommonNodes(); + // ============ UI 更新 ============ + + /// + /// 根据值和验证状态更新 UI 显示 + /// 子类可覆写以处理专属组件更新(Slider.value, Image.color 等) + /// 基类负责合成标签文本、值文本颜色、单位文本 + /// + protected virtual void UpdateValueDisplay(object value, ValidationState state) + { + // 1. 字段标签合成 + if (_fieldLabelText != null) + { + string label = BuildLabel(state); + _fieldLabelText.text = label; + } + + // 2. 值显示 + if (_valueDisplayText != null) + { + string display = CurrentAccessor?.FormatDisplay(value) ?? value?.ToString() ?? ""; + _valueDisplayText.text = display; + + _valueDisplayText.color = state == ValidationState.Invalid + ? Color.red + : state == ValidationState.Warning + ? new Color(1f, 0.92f, 0.016f) // yellow + : Color.white; + } + + // 3. 单位文本 + if (_unitText != null) + _unitText.text = BoundMetaData?.UnitLabelName ?? ""; } /// - /// 更新 UI 显示值(基类默认仅查找 Text/TMP_Text 设置文本) - /// 子类可覆写以实现专属更新逻辑(如 Slider.value, Image.color 等) + /// 合成标签文本:名称 + (只读) + 警告标记 /// - protected virtual void UpdateDisplayValue(object value) + protected string BuildLabel(ValidationState state) { - if (value == null) return; + var meta = BoundMetaData; + var sb = new StringBuilder(); + sb.Append(meta?.LabelName ?? meta?.MemberName ?? ""); -#if dUI_TextMeshPro - var tmpText = GetComponent(); - if (tmpText != null) - { - tmpText.text = CurrentAccessor?.FormatDisplay(value) ?? value.ToString(); - return; - } -#endif + // 必填标记 + if (meta != null && meta.Required) + sb.Append(" *"); - var uiText = GetComponent(); - if (uiText != null) + // 只读标记 + if (CurrentAccessor?.IsReadOnly == true) + sb.Append(" (只读)"); + + // 警告/无效标记(用富文本颜色) + if (state == ValidationState.Warning) + sb.Append(" ⚠警告"); + else if (state == ValidationState.Invalid) + sb.Append(" ⚠无效值"); + + return sb.ToString(); + } + + // ============ 验证状态回调 ============ + + /// + /// 验证状态变更回调 — 子类可覆写以实现专属样式切换 + /// 如 Slider 填充色变黄/红、InputField 文字变色等 + /// + protected virtual void OnValidationStateChanged(ValidationState state) + { + CurrentValidationState = state; + + // 刷新标签文本(警告/无效标记在标签上) + if (_fieldLabelText != null) + _fieldLabelText.text = BuildLabel(state); + + // 值文本颜色 + if (_valueDisplayText != null) { - uiText.text = CurrentAccessor?.FormatDisplay(value) ?? value.ToString(); + _valueDisplayText.color = state == ValidationState.Invalid + ? Color.red + : state == ValidationState.Warning + ? new Color(1f, 0.92f, 0.016f) + : Color.white; } } + // ============ 元数据广播 ============ + /// /// 由绑定系统调用,传递元数据并广播到 IXUIAvrgMDValueStyle 子组件 /// @@ -166,7 +247,6 @@ namespace XericUI.ReflectionUIGenerator if (metaData == null) return; - // 广播元数据到子节点样式组件 var styles = GetComponentsInChildren(true); foreach (var style in styles) { @@ -176,60 +256,5 @@ namespace XericUI.ReflectionUIGenerator style.ApplyMetaData(metaData); } } - - // ============ 样式读取 ============ - - /// - /// 从样式表获取值(自动使用 BoundMetaData.StyleNamespace) - /// - protected StyleValue GetStyleValue(StyleMember member) - { - if (member == null) return null; - string ns = BoundMetaData?.StyleNamespace ?? "default"; - return StyleManager.GetValue(ns, member.StylePath); - } - - // ============ 通用 UI 插槽刷新 ============ - - /// 刷新所有通用 UI 插槽(标签、单位、只读、警告标记) - protected void RefreshCommonNodes() - { - var meta = BoundMetaData; - if (meta == null) return; - - // 1. 字段标签节点 - if (fieldLabelNode != null) - { - string prefix = ""; - if (CurrentAccessor?.IsReadOnly == true) - { - var pv = GetStyleValue(_styleReadOnlyPrefix); - prefix = pv?.GetValueAsString() ?? "[只读] "; - } - SetNodeText(fieldLabelNode, prefix + (meta.LabelName ?? meta.MemberName)); - } - - // 2. 单位文本节点 - if (unitTextNode != null) - SetNodeText(unitTextNode, meta.UnitLabelName ?? ""); - - // 3. 只读标记节点 - if (readOnlyNode != null) - readOnlyNode.SetActive(CurrentAccessor?.IsReadOnly == true); - - // 4. warningNode 由外部验证系统触发,此方法不直接控制 - } - - /// 在节点及其子节点上查找 Text / TMP_Text 并设置文本 - protected static void SetNodeText(GameObject node, string text) - { - if (node == null) return; -#if dUI_TextMeshPro - var t = node.GetComponentInChildren(true); - if (t != null) { t.text = text; return; } -#endif - var ut = node.GetComponentInChildren(true); - if (ut != null) ut.text = text; - } } } diff --git a/Runtime/OverrideUI/AvrgValues/XUIAvrgMDVectorValue.cs b/Runtime/OverrideUI/AvrgValues/XUIAvrgMDVectorValue.cs index 470eb39..bf9188f 100644 --- a/Runtime/OverrideUI/AvrgValues/XUIAvrgMDVectorValue.cs +++ b/Runtime/OverrideUI/AvrgValues/XUIAvrgMDVectorValue.cs @@ -1,6 +1,5 @@ using UnityEngine; using UnityEngine.UI; -using XericLibrary.Runtime.SuperStyleSheet; namespace XericUI.ReflectionUIGenerator { @@ -11,37 +10,39 @@ namespace XericUI.ReflectionUIGenerator [AddComponentMenu("Xeric Library/UI Action Vessel/Values/XUIAvrgMDVectorValue")] public class XUIAvrgMDVectorValue : XUIAvrgMDValue { - [SerializeField] - [Tooltip("X 分量 InputField(可选,也可通过 BindingSlots 的子节点绑定)")] + [SerializeField, Tooltip("X 分量 InputField(可选,也可通过 BindingSlots 的子节点绑定)")] private InputField _inputX; - [SerializeField] - [Tooltip("Y 分量 InputField")] + [SerializeField, Tooltip("Y 分量 InputField")] private InputField _inputY; - [SerializeField] - [Tooltip("Z 分量 InputField(Vector3/Vector4 时使用)")] + [SerializeField, Tooltip("Z 分量 InputField(Vector3/Vector4 时使用)")] private InputField _inputZ; - [SerializeField] - [Tooltip("W 分量 InputField(Vector4 时使用)")] + [SerializeField, Tooltip("W 分量 InputField(Vector4 时使用)")] private InputField _inputW; - // ============ 样式表参数 ============ - private readonly StyleMember _styleLayoutDirection = new() { StylePath = "xuiavrg/vector/layoutDirection" }; + // ============ 对象池生命周期 ============ - // ============ 生命周期 ============ - - protected override void OnEnable() + public override void OnPoolGet(AvrgMemberMetaData metaData) { - base.OnEnable(); - _styleLayoutDirection.Register(); + base.OnPoolGet(metaData); + + int componentCount = GetComponentCount(metaData?.ValueType); + SetInputActive(_inputX, 0, componentCount); + SetInputActive(_inputY, 1, componentCount); + SetInputActive(_inputZ, 2, componentCount); + SetInputActive(_inputW, 3, componentCount); } - protected override void OnDisable() + public override void OnPoolRelease() { - base.OnDisable(); - _styleLayoutDirection.Unregister(); + if (_inputX != null) _inputX.onEndEdit.RemoveAllListeners(); + if (_inputY != null) _inputY.onEndEdit.RemoveAllListeners(); + if (_inputZ != null) _inputZ.onEndEdit.RemoveAllListeners(); + if (_inputW != null) _inputW.onEndEdit.RemoveAllListeners(); + + base.OnPoolRelease(); } // ============ 核心绑定 ============ @@ -56,18 +57,6 @@ namespace XericUI.ReflectionUIGenerator if (binding == null || accessor == null) return; - // 根据向量类型启用/禁用对应分量 - int componentCount = GetComponentCount(accessor.ValueType); - SetInputActive(_inputX, 0, componentCount); - SetInputActive(_inputY, 1, componentCount); - SetInputActive(_inputZ, 2, componentCount); - SetInputActive(_inputW, 3, componentCount); - - // 样式表控制排列方向 - var dirVal = GetStyleValue(_styleLayoutDirection); - string dir = dirVal?.GetValueAsString() ?? "Horizontal"; - ApplyLayoutDirection(dir); - // 为每个输入注册事件 BindInputField(_inputX, 0, accessor, binding, scheduler); BindInputField(_inputY, 1, accessor, binding, scheduler); @@ -75,41 +64,18 @@ namespace XericUI.ReflectionUIGenerator BindInputField(_inputW, 3, accessor, binding, scheduler); // 数据 → UI - binding.OnUIDataChanged += value => + binding.OnUIDataChanged += (value, state) => { UpdateAllInputs(value); + base.UpdateValueDisplay(value, state); }; + binding.OnValidationStateChanged += OnValidationStateChanged; + // 首次同步 var initialValue = accessor.GetValue(); UpdateAllInputs(initialValue); - - RefreshCommonNodes(); - } - - // ============ 布局 ============ - - private void ApplyLayoutDirection(string dir) - { - var layout = GetComponent(); - if (layout == null) return; - - if (dir == "Vertical") - { - if (layout is HorizontalLayoutGroup) - { - Destroy(layout); - gameObject.AddComponent(); - } - } - else - { - if (layout is VerticalLayoutGroup) - { - Destroy(layout); - gameObject.AddComponent(); - } - } + UpdateValueDisplay(initialValue, ValidationState.Normal); } // ============ 输入绑定 ============