#nullable enable using System; using UnityEngine; using UnityEngine.UIElements; namespace UVC.UIToolkit { /// /// FloatRange 속성 View 클래스입니다. /// Min, Max 두 개의 UTKFloatField를 사용하여 실수 범위를 표시/편집합니다. /// [UxmlElement] public partial class UTKFloatRangePropertyItemView : UTKPropertyItemViewBase, IUTKPropertyItemView { #region Fields private UTKFloatField? _minField; private UTKFloatField? _maxField; private UTKFloatRange _value; private IUTKPropertyItem? _boundData; #endregion #region Properties protected override string ViewTypeName => "UTKFloatRangePropertyItemView"; /// 현재 값 public UTKFloatRange Value { get => _value; set { if (!_value.Equals(value)) { _value = value; UpdateValueUI(); OnValueChanged?.Invoke(value); if (_boundData != null && !_boundData.Value.Equals(value)) { _boundData.Value = value; } } } } #endregion #region Events public event Action? OnValueChanged; #endregion #region Constructor public UTKFloatRangePropertyItemView() : base() { InitializeUI(); } public UTKFloatRangePropertyItemView(string label, UTKFloatRange value = default) : base() { _value = value; Label = label; InitializeUI(); } public UTKFloatRangePropertyItemView(string label, float min, float max) : base() { _value = new UTKFloatRange(min, max); Label = label; InitializeUI(); } #endregion #region Initialization private void InitializeUI() { AddToClassList("utk-property-item-view"); AddToClassList("utk-property-item-view--float-range"); if (!CreateUIFromUxml()) { CreateUIFallback(); } // UXML에서 요소 가져오기 QueryUIElements(); // 이벤트 등록 RegisterEvents(); UpdateValueUI(); UpdateReadOnlyState(); } private void QueryUIElements() { _minField = this.Q("min-field"); _maxField = this.Q("max-field"); // Fallback: UXML에서 못 찾으면 생성 if (_valueContainer != null) { _valueContainer.style.flexDirection = FlexDirection.Row; if (_minField == null) { _minField = new UTKFloatField { name = "min-field" }; _minField.style.flexGrow = 1; _minField.AddToClassList("utk-property-item-view__field"); _valueContainer.Add(_minField); var separator = new UTKLabel("~", UTKLabel.LabelSize.Body2); separator.AddToClassList("utk-property-item-view__range-separator"); _valueContainer.Add(separator); } if (_maxField == null) { _maxField = new UTKFloatField { name = "max-field" }; _maxField.style.flexGrow = 1; _maxField.AddToClassList("utk-property-item-view__field"); _valueContainer.Add(_maxField); } } // 초기 값 설정 if (_minField != null) { _minField.SetValueWithoutNotify(_value.Min); _minField.isReadOnly = IsReadOnly; } if (_maxField != null) { _maxField.SetValueWithoutNotify(_value.Max); _maxField.isReadOnly = IsReadOnly; } } private void RegisterEvents() { if (_minField != null) { _minField.OnValueChanged += OnMinChanged; } if (_maxField != null) { _maxField.OnValueChanged += OnMaxChanged; } } private void UnregisterEvents() { if (_minField != null) { _minField.OnValueChanged -= OnMinChanged; } if (_maxField != null) { _maxField.OnValueChanged -= OnMaxChanged; } } #endregion #region Override Methods protected override void CreateValueUI(VisualElement container) { // UXML/QueryUIElements 기반으로 생성하므로 여기서는 생성하지 않음 } public override void RefreshUI() { UpdateValueUI(); } protected override void OnReadOnlyStateChanged(bool isReadOnly) { if (_minField != null) _minField.isReadOnly = isReadOnly; if (_maxField != null) _maxField.isReadOnly = isReadOnly; } #endregion #region Event Handling private void OnMinChanged(float newMin) { var newValue = new UTKFloatRange(newMin, _value.Max); if (!_value.Equals(newValue)) { _value = newValue; OnValueChanged?.Invoke(newValue); if (_boundData != null && !_boundData.Value.Equals(newValue)) { _boundData.Value = newValue; } } } private void OnMaxChanged(float newMax) { var newValue = new UTKFloatRange(_value.Min, newMax); if (!_value.Equals(newValue)) { _value = newValue; OnValueChanged?.Invoke(newValue); if (_boundData != null && !_boundData.Value.Equals(newValue)) { _boundData.Value = newValue; } } } #endregion #region Value Update private void UpdateValueUI() { if (_minField != null && !Mathf.Approximately(_minField.Value, _value.Min)) { _minField.SetValueWithoutNotify(_value.Min); } if (_maxField != null && !Mathf.Approximately(_maxField.Value, _value.Max)) { _maxField.SetValueWithoutNotify(_value.Max); } } #endregion #region Data Binding public void Bind(IUTKPropertyItem data) { if (data is IUTKPropertyItem rangeData) { Bind(rangeData); } else { Debug.LogWarning($"[UTKFloatRangePropertyItemView] Cannot bind to non-FloatRange data: {data.GetType().Name}"); } } public void Bind(IUTKPropertyItem data) { Unbind(); _boundData = data; Label = data.Name; _value = data.Value; IsReadOnly = data.IsReadOnly; IsVisible = data.IsVisible; TooltipText = data.Tooltip; data.OnTypedValueChanged += OnDataValueChanged; UpdateValueUI(); UpdateReadOnlyState(); } public void Unbind() { if (_boundData != null) { _boundData.OnTypedValueChanged -= OnDataValueChanged; _boundData = null; } } private void OnDataValueChanged(IUTKPropertyItem item, UTKFloatRange oldValue, UTKFloatRange newValue) { if (!_value.Equals(newValue)) { _value = newValue; UpdateValueUI(); } } #endregion #region Dispose protected override void Dispose(bool disposing) { if (_disposed) return; if (disposing) { UnregisterEvents(); Unbind(); OnValueChanged = null; _minField = null; _maxField = null; } base.Dispose(disposing); } #endregion } }