using System; using System.Collections.Generic; using System.Collections; using System.Linq; using Rive; using Rive.Components; using UnityEditor; using UnityEditor.UIElements; using UnityEngine; using UnityEngine.UIElements; namespace Rive.EditorTools { internal class DataBindingPlaygroundWindow : EditorWindow { private RiveWidget m_widget; private ObjectField m_widgetField; private HelpBox m_playModeHelpBox; private VisualElement m_interactiveContainer; private ScrollView m_propertiesScroll; private readonly List m_propertyBindings = new List(); private readonly List m_listBindings = new List(); private readonly List m_triggerBindingDisposers = new List(); private bool m_isRefreshing; private const double TriggerFiredHighlightSeconds = 0.75d; private enum PlaygroundState { NotPlaying, NoWidget, NoFileMetadata, NoViewModels, NoArtboardMetadata, NoDefaultViewModel, WidgetNotLoaded, NoViewModelInstance, Ready } private FileMetadata m_fileMetadata; private FileMetadata.ArtboardMetadata m_artboardMetadata; private double m_nextRefreshTime; private readonly Dictionary m_imageSelectionCache = new Dictionary(); private readonly Dictionary m_artboardSelectionCache = new Dictionary(); private readonly Dictionary m_viewModelExpansion = new Dictionary(); // Used for the artboard databinding dropdowns private enum ArtboardViewModelBindingMode { ExistingInstance, CreateNewInstance } private class ArtboardSelection { public Asset Asset; public File File; public string ArtboardName; public ArtboardViewModelBindingMode ViewModelBindingMode = ArtboardViewModelBindingMode.ExistingInstance; public string ExistingViewModelInstanceName; public ViewModelInstance CustomViewModelInstance; public readonly List CustomBindings = new List(); public readonly List CustomListBindings = new List(); public void DisposeCustomViewModelInstance() { CustomViewModelInstance?.Dispose(); CustomViewModelInstance = null; CustomBindings.Clear(); CustomListBindings.Clear(); } public void DisposeAll() { DisposeCustomViewModelInstance(); File = null; } } private const string DocsBaseUrl = InspectorDocLinks.UnityDataBinding; private class PropertyBinding { public string Path; public VisualElement Control; public Action Sync; } private class ListPropertyBinding { public string Path; public VisualElement Control; public Action Sync; public List LastItems = new List(); public bool InitializedTypeSelection; } // Adapter that proxies a list property directly into a ListView without keeping a stale mirror. private class ListPropertyAdapter : IList, IList { private readonly Func m_propertyGetter; private readonly Func m_factory; public ListPropertyAdapter(Func propertyGetter, Func factory) { m_propertyGetter = propertyGetter; m_factory = factory; } private ViewModelInstanceListProperty Prop => m_propertyGetter?.Invoke(); public int Count => Prop?.Count ?? 0; public bool IsReadOnly => false; bool IList.IsFixedSize => false; public ViewModelInstance this[int index] { get => Prop?.GetInstanceAt(index); set => MoveOrReplace(index, value); } object IList.this[int index] { get => this[index]; set { MoveOrReplace(index, value as ViewModelInstance); } } private void MoveOrReplace(int index, ViewModelInstance value) { var prop = Prop; if (prop == null || value == null) { return; } // If the instance already exists in the list, move it. int existingIndex = -1; for (int i = 0; i < prop.Count; i++) { if (ReferenceEquals(prop.GetInstanceAt(i), value)) { existingIndex = i; break; } } if (existingIndex == index) { return; } if (existingIndex >= 0) { prop.RemoveAt(existingIndex); if (existingIndex < index) { index -= 1; } } else if (index < prop.Count) { // Replace the current item at index if value not found elsewhere prop.RemoveAt(index); } index = Mathf.Clamp(index, 0, prop.Count); prop.Insert(value, index); } public void Add(ViewModelInstance item) { var prop = Prop; if (prop == null) { return; } var toAdd = item ?? m_factory?.Invoke(); if (toAdd != null) { prop.Add(toAdd); } } int IList.Add(object value) { var prop = Prop; if (prop == null) { return -1; } Add(value as ViewModelInstance); return prop.Count - 1; } public void Clear() { var prop = Prop; if (prop == null) { return; } for (int i = prop.Count - 1; i >= 0; i--) { prop.RemoveAt(i); } } public bool Contains(ViewModelInstance item) { var prop = Prop; if (prop == null || item == null) { return false; } for (int i = 0; i < prop.Count; i++) { if (ReferenceEquals(prop.GetInstanceAt(i), item)) { return true; } } return false; } public void CopyTo(ViewModelInstance[] array, int arrayIndex) { var prop = Prop; if (prop == null || array == null) { return; } int count = prop.Count; for (int i = 0; i < count && arrayIndex + i < array.Length; i++) { array[arrayIndex + i] = prop.GetInstanceAt(i); } } public IEnumerator GetEnumerator() { var prop = Prop; if (prop == null) { yield break; } int count = prop.Count; for (int i = 0; i < count; i++) { yield return prop.GetInstanceAt(i); } } public int IndexOf(ViewModelInstance item) { var prop = Prop; if (prop == null || item == null) { return -1; } for (int i = 0; i < prop.Count; i++) { if (ReferenceEquals(prop.GetInstanceAt(i), item)) { return i; } } return -1; } public void Insert(int index, ViewModelInstance item) { var prop = Prop; if (prop == null) { return; } var toInsert = item ?? m_factory?.Invoke(); if (toInsert == null) { return; } index = Mathf.Clamp(index, 0, prop.Count); prop.Insert(toInsert, index); } void IList.Insert(int index, object value) => Insert(index, value as ViewModelInstance); public bool Remove(ViewModelInstance item) { var prop = Prop; if (prop == null || item == null) { return false; } prop.Remove(item); return true; } void IList.Remove(object value) => Remove(value as ViewModelInstance); public void RemoveAt(int index) { var prop = Prop; if (prop == null || index < 0 || index >= prop.Count) { return; } prop.RemoveAt(index); } bool IList.Contains(object value) => Contains(value as ViewModelInstance); void ICollection.CopyTo(Array array, int index) { if (array == null) { return; } var prop = Prop; if (prop == null) { return; } for (int i = 0; i < prop.Count && index + i < array.Length; i++) { array.SetValue(prop.GetInstanceAt(i), index + i); } } bool ICollection.IsSynchronized => false; private readonly object m_syncRoot = new object(); object ICollection.SyncRoot => m_syncRoot; int IList.IndexOf(object value) => IndexOf(value as ViewModelInstance); System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() => GetEnumerator(); } public static void Open(RiveWidget widget) { var window = GetWindow(); window.titleContent = new GUIContent("Rive Data Binding Playground"); window.minSize = new Vector2(420, 360); window.SetTarget(widget); window.Show(); } private void OnEnable() { titleContent = new GUIContent("Rive Data Binding Playground"); minSize = new Vector2(420, 360); BuildLayout(); EditorApplication.update += EditorUpdate; } private void OnDisable() { EditorApplication.update -= EditorUpdate; ClearTriggerBindings(); m_propertyBindings.Clear(); m_listBindings.Clear(); m_widget = null; foreach (var selection in m_artboardSelectionCache.Values) { selection.DisposeAll(); } m_artboardSelectionCache.Clear(); m_viewModelExpansion.Clear(); } private void EditorUpdate() { // throttle refreshes to avoid unnecessary work if (EditorApplication.timeSinceStartup < m_nextRefreshTime) { return; } m_nextRefreshTime = EditorApplication.timeSinceStartup + 0.25f; UpdateVisibility(); RefreshValues(); } private void BuildLayout() { var root = rootVisualElement; root.Clear(); root.style.paddingLeft = 10; root.style.paddingRight = 10; root.style.paddingTop = 8; root.style.paddingBottom = 10; var subtitleRow = new VisualElement { style = { flexDirection = FlexDirection.Row, alignItems = Align.Center, marginBottom = 14 } }; var subtitle = new Label("Inspect and tweak ViewModel instance properties for the selected RiveWidget."); subtitle.style.flexGrow = 1; subtitle.style.fontSize = 12; subtitle.style.color = new UnityEngine.Color(0.8f, 0.8f, 0.8f, 0.95f); var docsLink = new Label("View Data Binding Docs"); StyleLinkLabel(docsLink, InspectorDocLinks.UnityDataBinding); subtitleRow.Add(subtitle); subtitleRow.Add(docsLink); root.Add(subtitleRow); m_playModeHelpBox = new HelpBox( "Play with data binding values for the selected RiveWidget while in Play Mode. " + "Changes are applied to the widget's current ViewModel instance.", HelpBoxMessageType.Info); m_playModeHelpBox.style.marginBottom = 10; root.Add(m_playModeHelpBox); m_widgetField = new ObjectField("Widget") { objectType = typeof(RiveWidget), allowSceneObjects = true }; m_widgetField.RegisterValueChangedCallback(evt => { SetTarget(evt.newValue as RiveWidget); }); root.Add(m_widgetField); m_interactiveContainer = new VisualElement(); m_interactiveContainer.style.flexDirection = FlexDirection.Column; m_interactiveContainer.style.flexGrow = 1; root.Add(m_interactiveContainer); m_propertiesScroll = new ScrollView { style = { flexGrow = 1 } }; m_interactiveContainer.Add(m_propertiesScroll); UpdateVisibility(); } private void SetTarget(RiveWidget widget) { m_widget = widget; m_widgetField?.SetValueWithoutNotify(widget); RefreshMetadata(); RebuildProperties(); UpdateVisibility(); RefreshValues(); } private void RefreshMetadata() { m_fileMetadata = m_widget?.Asset?.EditorOnlyMetadata; m_artboardMetadata = null; if (m_fileMetadata == null) { return; } var artboardName = !string.IsNullOrEmpty(m_widget?.ArtboardName) ? m_widget.ArtboardName : m_fileMetadata.GetArtboardNames().FirstOrDefault(); if (!string.IsNullOrEmpty(artboardName)) { m_artboardMetadata = m_fileMetadata.GetArtboard(artboardName); } } private void RebuildProperties() { ClearTriggerBindings(); m_propertyBindings.Clear(); m_listBindings.Clear(); m_viewModelExpansion.Clear(); m_propertiesScroll.Clear(); if (m_widget == null) { m_propertiesScroll.Add(new Label("Select a RiveWidget to get started.")); return; } if (m_artboardMetadata == null) { m_propertiesScroll.Add(new HelpBox("No artboard metadata found for the selected widget.", HelpBoxMessageType.Warning)); return; } if (m_artboardMetadata.DefaultViewModel == null) { m_propertiesScroll.Add(new HelpBox("The current artboard does not have a default ViewModel.", HelpBoxMessageType.Warning)); return; } BuildViewModelSection( m_artboardMetadata.DefaultViewModel, string.Empty, m_propertiesScroll, 0); } private FileMetadata.ViewModelMetadata FindViewModelMetadata(string viewModelName, FileMetadata metadataContext = null) { var metadata = metadataContext ?? m_fileMetadata; if (metadata == null || string.IsNullOrEmpty(viewModelName)) { return null; } return metadata.ViewModels.FirstOrDefault(vm => vm.Name == viewModelName); } private List GetEnumOptions(FileMetadata.ViewModelPropertyMetadata property, FileMetadata metadataContext = null) { var metadata = metadataContext ?? m_fileMetadata; if (metadata == null || metadata.Enums == null || string.IsNullOrEmpty(property.EnumTypeName)) { return null; } var enumMeta = metadata.Enums.FirstOrDefault(e => e.Name == property.EnumTypeName); return enumMeta?.Values?.ToList(); } private void AddStringField(VisualElement parent, string path, string displayName, Func instanceProvider = null, string cacheKey = null, string pathLabelOverride = null, List bindingList = null) { instanceProvider ??= GetCurrentInstance; cacheKey ??= path; bindingList ??= m_propertyBindings; var field = new TextField(); field.RegisterValueChangedCallback(evt => { var instance = instanceProvider(); var prop = instance?.GetStringProperty(path); if (prop != null) { prop.Value = evt.newValue; } }); bindingList.Add(new PropertyBinding { Path = cacheKey, Control = field, Sync = instance => { var current = instanceProvider(); var prop = current?.GetStringProperty(path); field.SetEnabled(prop != null); if (prop != null) { field.SetValueWithoutNotify(prop.Value ?? string.Empty); } } }); parent.Add(CreatePropertyCard(displayName, pathLabelOverride ?? path, "String", GetDocUrl(ViewModelDataType.String), null, field)); } private void AddNumberField(VisualElement parent, string path, string displayName, Func instanceProvider = null, string cacheKey = null, string pathLabelOverride = null, List bindingList = null) { instanceProvider ??= GetCurrentInstance; cacheKey ??= path; bindingList ??= m_propertyBindings; var field = new FloatField(); field.RegisterValueChangedCallback(evt => { var instance = instanceProvider(); var prop = instance?.GetNumberProperty(path); if (prop != null) { prop.Value = evt.newValue; } }); bindingList.Add(new PropertyBinding { Path = cacheKey, Control = field, Sync = instance => { var current = instanceProvider(); var prop = current?.GetNumberProperty(path); field.SetEnabled(prop != null); if (prop != null) { field.SetValueWithoutNotify(prop.Value); } } }); parent.Add(CreatePropertyCard(displayName, pathLabelOverride ?? path, "Number", GetDocUrl(ViewModelDataType.Number), null, field)); } private void AddBooleanField(VisualElement parent, string path, string displayName, Func instanceProvider = null, string cacheKey = null, string pathLabelOverride = null, List bindingList = null) { instanceProvider ??= GetCurrentInstance; cacheKey ??= path; bindingList ??= m_propertyBindings; var toggle = new Toggle(); toggle.RegisterValueChangedCallback(evt => { var instance = instanceProvider(); var prop = instance?.GetBooleanProperty(path); if (prop != null) { prop.Value = evt.newValue; } }); bindingList.Add(new PropertyBinding { Path = cacheKey, Control = toggle, Sync = instance => { var current = instanceProvider(); var prop = current?.GetBooleanProperty(path); toggle.SetEnabled(prop != null); if (prop != null) { toggle.SetValueWithoutNotify(prop.Value); } } }); parent.Add(CreatePropertyCard(displayName, pathLabelOverride ?? path, "Boolean", GetDocUrl(ViewModelDataType.Boolean), null, toggle)); } private void AddColorField(VisualElement parent, string path, string displayName, Func instanceProvider = null, string cacheKey = null, string pathLabelOverride = null, List bindingList = null) { instanceProvider ??= GetCurrentInstance; cacheKey ??= path; bindingList ??= m_propertyBindings; var field = new ColorField(); field.RegisterValueChangedCallback(evt => { var instance = instanceProvider(); var prop = instance?.GetColorProperty(path); if (prop != null) { prop.Value = evt.newValue; } }); bindingList.Add(new PropertyBinding { Path = cacheKey, Control = field, Sync = instance => { var current = instanceProvider(); var prop = current?.GetColorProperty(path); field.SetEnabled(prop != null); if (prop != null) { field.SetValueWithoutNotify(prop.Value); } } }); parent.Add(CreatePropertyCard(displayName, pathLabelOverride ?? path, "Color", GetDocUrl(ViewModelDataType.Color), null, field)); } private void AddImageField(VisualElement parent, string path, string displayName, Func instanceProvider = null, string cacheKey = null, string pathLabelOverride = null, List bindingList = null) { instanceProvider ??= GetCurrentInstance; cacheKey ??= path; bindingList ??= m_propertyBindings; var field = new ObjectField { objectType = typeof(ImageOutOfBandAsset), allowSceneObjects = false }; field.RegisterValueChangedCallback(evt => { var instance = instanceProvider(); var prop = instance?.GetImageProperty(path); if (prop == null) { return; } var asset = evt.newValue as ImageOutOfBandAsset; if (asset == null) { m_imageSelectionCache[cacheKey] = null; prop.Value = null; return; } m_imageSelectionCache[cacheKey] = asset; asset.Load(); prop.Value = asset; asset.Unload(); }); bindingList.Add(new PropertyBinding { Path = cacheKey, Control = field, Sync = instance => { var current = instanceProvider(); var prop = current?.GetImageProperty(path); field.SetEnabled(prop != null); // Reflect cached selection for UX (no getter available from runtime) if (m_imageSelectionCache.TryGetValue(cacheKey, out var cached)) { field.SetValueWithoutNotify(cached); } else { field.SetValueWithoutNotify(null); } } }); parent.Add(CreatePropertyCard(displayName, pathLabelOverride ?? path, "Image", GetDocUrl(ViewModelDataType.AssetImage), null, field)); } private void AddEnumField(VisualElement parent, string path, FileMetadata.ViewModelPropertyMetadata property, Func instanceProvider = null, string cacheKey = null, string pathLabelOverride = null, List bindingList = null, FileMetadata metadataContext = null) { instanceProvider ??= GetCurrentInstance; cacheKey ??= path; bindingList ??= m_propertyBindings; var options = GetEnumOptions(property, metadataContext) ?? new List(); var defaultOption = options.Count > 0 ? options[0] : string.Empty; var popup = new PopupField(null, options, defaultOption); popup.RegisterValueChangedCallback(evt => { if (string.IsNullOrEmpty(evt.newValue)) { return; } var instance = instanceProvider(); var prop = instance?.GetEnumProperty(path); if (prop != null) { prop.Value = evt.newValue; } }); bindingList.Add(new PropertyBinding { Path = cacheKey, Control = popup, Sync = instance => { var currentInstance = instanceProvider(); var prop = currentInstance?.GetEnumProperty(path); bool hasProp = prop != null; popup.SetEnabled(hasProp && popup.choices.Count > 0); if (!hasProp) { return; } var currentValue = prop.Value; if (!options.Contains(currentValue)) { options = prop.EnumValues?.Where(v => !string.IsNullOrEmpty(v)).ToList() ?? options; popup.choices = options; popup.SetEnabled(options.Count > 0); } if (!string.IsNullOrEmpty(currentValue) && options.Contains(currentValue)) { popup.SetValueWithoutNotify(currentValue); } else if (options.Count > 0) { popup.SetValueWithoutNotify(options[0]); } } }); parent.Add(CreatePropertyCard(property.Name, pathLabelOverride ?? path, $"Enum ({property.EnumTypeName})", GetDocUrl(ViewModelDataType.Enum), null, popup)); } private ArtboardSelection GetOrCreateArtboardSelection(string path) { if (!m_artboardSelectionCache.TryGetValue(path, out var selection)) { selection = new ArtboardSelection(); m_artboardSelectionCache[path] = selection; } return selection; } private void DisposeArtboardSelection(string path) { if (m_artboardSelectionCache.TryGetValue(path, out var selection)) { selection.DisposeAll(); } } private void AddArtboardField(VisualElement parent, string path, string displayName, Func instanceProvider = null, string cacheKey = null, string pathLabelOverride = null, List bindingList = null) { instanceProvider ??= GetCurrentInstance; cacheKey ??= path; bindingList ??= m_propertyBindings; var selection = GetOrCreateArtboardSelection(cacheKey); var assetField = new ObjectField { objectType = typeof(Asset), allowSceneObjects = false, label = "Rive File", tooltip = "Select the Rive file containing the desired artboard to bind" }; assetField.style.marginLeft = 0; var artboardDropdown = new DropdownField { choices = new List(), label = "Artboard", tooltip = "Select the artboard to bind from the selected Rive file", value = null }; artboardDropdown.style.marginLeft = 0; var vmBindingModeOptions = new List { "Use Existing Instance", "New Instance" }; var vmBindingModeDropdown = new DropdownField( "View Model Binding", vmBindingModeOptions, vmBindingModeOptions[(int)selection.ViewModelBindingMode]) { tooltip = "Select whether to bind using a named instance from the selected artboard's default ViewModel or create a new instance" }; vmBindingModeDropdown.style.marginLeft = 0; vmBindingModeDropdown.style.display = DisplayStyle.None; var vmInstanceDropdown = new DropdownField { choices = new List(), label = "Instance", tooltip = "Select a named instance from the artboard's default ViewModel", value = null }; vmInstanceDropdown.style.marginLeft = 0; vmInstanceDropdown.style.display = DisplayStyle.None; var vmHintHelpBox = new HelpBox(string.Empty, HelpBoxMessageType.Info); vmHintHelpBox.style.display = DisplayStyle.None; vmHintHelpBox.style.marginTop = 4; var customVmContainer = new VisualElement(); customVmContainer.style.flexDirection = FlexDirection.Column; customVmContainer.style.display = DisplayStyle.None; customVmContainer.style.marginTop = 6; FileMetadata.ArtboardMetadata GetSelectedArtboardMetadata() { var metadata = selection.Asset?.EditorOnlyMetadata; if (metadata == null || string.IsNullOrEmpty(selection.ArtboardName)) { return null; } return metadata.GetArtboard(selection.ArtboardName); } FileMetadata.ViewModelMetadata GetDefaultViewModelMetadata() { return GetSelectedArtboardMetadata()?.DefaultViewModel; } ViewModel GetDefaultViewModelFromFile() { var vmMeta = GetDefaultViewModelMetadata(); if (selection.File == null || vmMeta == null || string.IsNullOrEmpty(vmMeta.Name)) { return null; } return selection.File.GetViewModelByName(vmMeta.Name); } void EnsureCustomViewModelInstance() { if (selection.CustomViewModelInstance != null) { return; } var defaultVm = GetDefaultViewModelFromFile(); if (defaultVm == null) { return; } selection.CustomViewModelInstance = defaultVm.CreateInstance(); } void RebuildCustomInstanceEditor() { selection.CustomBindings.Clear(); selection.CustomListBindings.Clear(); customVmContainer.Clear(); var defaultVmMeta = GetDefaultViewModelMetadata(); if (defaultVmMeta == null || selection.CustomViewModelInstance == null) { return; } BuildViewModelSection( defaultVmMeta, string.Empty, customVmContainer, 0, () => selection.CustomViewModelInstance, displayPathPrefix: "custom-instance", cachePathPrefix: $"{cacheKey}/custom-instance", bindingList: selection.CustomBindings, listBindingList: selection.CustomListBindings, metadataContext: selection.Asset?.EditorOnlyMetadata, viewModelResolver: name => string.IsNullOrEmpty(name) ? null : selection.File?.GetViewModelByName(name)); } void ApplySelectionToProperty() { var instance = instanceProvider(); var prop = instance?.GetArtboardProperty(path); if (prop == null) { return; } if (selection.File == null || string.IsNullOrEmpty(selection.ArtboardName)) { prop.Value = null; return; } BindableArtboard bindable = null; var defaultVmMeta = GetDefaultViewModelMetadata(); bool hasDefaultVm = defaultVmMeta != null && !string.IsNullOrEmpty(defaultVmMeta.Name); if (hasDefaultVm) { ViewModelInstance targetInstance = null; if (selection.ViewModelBindingMode == ArtboardViewModelBindingMode.ExistingInstance) { var vm = GetDefaultViewModelFromFile(); if (vm != null && !string.IsNullOrEmpty(selection.ExistingViewModelInstanceName)) { targetInstance = vm.CreateInstanceByName(selection.ExistingViewModelInstanceName); } } else { EnsureCustomViewModelInstance(); targetInstance = selection.CustomViewModelInstance; } bindable = selection.File.BindableArtboard(selection.ArtboardName, targetInstance); } else { bindable = selection.File.BindableArtboard(selection.ArtboardName); } if (bindable != null) { prop.Value = bindable; } } void RefreshViewModelControls() { var defaultVmMeta = GetDefaultViewModelMetadata(); bool hasDefaultVm = defaultVmMeta != null && !string.IsNullOrEmpty(defaultVmMeta.Name); vmBindingModeDropdown.style.display = hasDefaultVm ? DisplayStyle.Flex : DisplayStyle.None; vmHintHelpBox.style.display = hasDefaultVm ? DisplayStyle.None : (selection.Asset == null ? DisplayStyle.None : DisplayStyle.Flex); vmHintHelpBox.text = "This artboard doesn't support databinding."; if (!hasDefaultVm) { vmInstanceDropdown.style.display = DisplayStyle.None; customVmContainer.style.display = DisplayStyle.None; selection.ExistingViewModelInstanceName = null; selection.DisposeCustomViewModelInstance(); return; } var instanceNames = defaultVmMeta.InstanceNames ?? new List(); vmInstanceDropdown.choices = instanceNames.ToList(); if (!string.IsNullOrEmpty(selection.ExistingViewModelInstanceName) && vmInstanceDropdown.choices.Contains(selection.ExistingViewModelInstanceName)) { vmInstanceDropdown.SetValueWithoutNotify(selection.ExistingViewModelInstanceName); } else { selection.ExistingViewModelInstanceName = vmInstanceDropdown.choices.FirstOrDefault(); vmInstanceDropdown.SetValueWithoutNotify(selection.ExistingViewModelInstanceName); } vmBindingModeDropdown.SetValueWithoutNotify(vmBindingModeOptions[(int)selection.ViewModelBindingMode]); bool isExistingMode = selection.ViewModelBindingMode == ArtboardViewModelBindingMode.ExistingInstance; vmInstanceDropdown.style.display = isExistingMode ? DisplayStyle.Flex : DisplayStyle.None; vmInstanceDropdown.SetEnabled(isExistingMode && vmInstanceDropdown.choices.Count > 0); if (isExistingMode) { customVmContainer.style.display = DisplayStyle.None; } else { EnsureCustomViewModelInstance(); if (customVmContainer.childCount == 0) { RebuildCustomInstanceEditor(); } customVmContainer.style.display = selection.CustomViewModelInstance != null ? DisplayStyle.Flex : DisplayStyle.None; } } void PopulateArtboards(Asset asset) { artboardDropdown.choices = new List(); artboardDropdown.value = null; selection.ArtboardName = null; artboardDropdown.style.display = asset == null ? DisplayStyle.None : DisplayStyle.Flex; if (asset == null) { return; } var names = asset.EditorOnlyMetadata?.GetArtboardNames() ?? Array.Empty(); artboardDropdown.choices = names.ToList(); if (names.Length > 0) { var chosenArtboard = selection.ArtboardName; if (string.IsNullOrEmpty(chosenArtboard) || !names.Contains(chosenArtboard)) { chosenArtboard = names[0]; } selection.ArtboardName = chosenArtboard; artboardDropdown.value = chosenArtboard; } } assetField.RegisterValueChangedCallback(evt => { var newAsset = evt.newValue as Asset; if (!ReferenceEquals(selection.Asset, newAsset)) { DisposeArtboardSelection(cacheKey); selection.Asset = newAsset; selection.File = null; selection.ArtboardName = null; } PopulateArtboards(newAsset); if (newAsset != null) { selection.File = File.Load(newAsset); } RefreshViewModelControls(); ApplySelectionToProperty(); }); artboardDropdown.RegisterValueChangedCallback(evt => { selection.ArtboardName = evt.newValue; selection.DisposeCustomViewModelInstance(); customVmContainer.Clear(); RefreshViewModelControls(); ApplySelectionToProperty(); }); vmBindingModeDropdown.RegisterValueChangedCallback(evt => { selection.ViewModelBindingMode = evt.newValue == vmBindingModeOptions[(int)ArtboardViewModelBindingMode.CreateNewInstance] ? ArtboardViewModelBindingMode.CreateNewInstance : ArtboardViewModelBindingMode.ExistingInstance; if (selection.ViewModelBindingMode == ArtboardViewModelBindingMode.CreateNewInstance) { selection.DisposeCustomViewModelInstance(); customVmContainer.Clear(); } RefreshViewModelControls(); ApplySelectionToProperty(); }); vmInstanceDropdown.RegisterValueChangedCallback(evt => { selection.ExistingViewModelInstanceName = evt.newValue; ApplySelectionToProperty(); }); bindingList.Add(new PropertyBinding { Path = cacheKey, Control = artboardDropdown, Sync = instance => { var current = instanceProvider(); var prop = current?.GetArtboardProperty(path); bool hasProp = prop != null; assetField.SetEnabled(hasProp); artboardDropdown.SetEnabled(hasProp && selection.Asset != null); vmBindingModeDropdown.SetEnabled(hasProp); artboardDropdown.style.display = selection.Asset == null ? DisplayStyle.None : DisplayStyle.Flex; if (assetField.value != selection.Asset) { assetField.SetValueWithoutNotify(selection.Asset); } // Ensure file is loaded if we have an asset but no file yet if (hasProp && selection.Asset != null && selection.File == null) { selection.File = File.Load(selection.Asset); } // Populate dropdown if empty but we have asset metadata if (artboardDropdown.choices.Count == 0 && selection.Asset != null) { PopulateArtboards(selection.Asset); } // Keep dropdown selection in sync with cached choice if (selection.ArtboardName != null && artboardDropdown.value != selection.ArtboardName) { artboardDropdown.SetValueWithoutNotify(selection.ArtboardName); } RefreshViewModelControls(); foreach (var customBinding in selection.CustomBindings) { customBinding.Sync?.Invoke(selection.CustomViewModelInstance); } foreach (var customListBinding in selection.CustomListBindings) { customListBinding.Sync?.Invoke(selection.CustomViewModelInstance); } } }); var container = new VisualElement(); container.style.flexDirection = FlexDirection.Column; container.Add(assetField); container.Add(artboardDropdown); container.Add(vmBindingModeDropdown); container.Add(vmInstanceDropdown); container.Add(vmHintHelpBox); container.Add(customVmContainer); parent.Add(CreatePropertyCard(displayName, pathLabelOverride ?? path, "Artboard", GetDocUrl(ViewModelDataType.Artboard), null, container)); } private void AddTriggerField(VisualElement parent, string path, string displayName, Func instanceProvider = null, string cacheKey = null, string pathLabelOverride = null, List bindingList = null) { instanceProvider ??= GetCurrentInstance; cacheKey ??= path; bindingList ??= m_propertyBindings; ViewModelInstanceTriggerProperty subscribedProp = null; double lastTriggeredAt = -1d; var button = new Button(() => { var instance = instanceProvider(); var prop = instance?.GetTriggerProperty(path); prop?.Trigger(); }) { text = $"Fire Trigger" }; var statusLabel = new Label("Fired"); statusLabel.style.unityTextAlign = TextAnchor.MiddleCenter; statusLabel.style.paddingLeft = 8; statusLabel.style.paddingRight = 8; statusLabel.style.paddingTop = 2; statusLabel.style.paddingBottom = 2; statusLabel.style.marginLeft = 8; statusLabel.style.borderTopLeftRadius = 4; statusLabel.style.borderTopRightRadius = 4; statusLabel.style.borderBottomLeftRadius = 4; statusLabel.style.borderBottomRightRadius = 4; statusLabel.style.display = DisplayStyle.None; Action onTriggered = () => { lastTriggeredAt = EditorApplication.timeSinceStartup; statusLabel.text = "Fired"; statusLabel.style.color = new UnityEngine.Color(0.12f, 0.95f, 0.45f, 1f); statusLabel.style.backgroundColor = new UnityEngine.Color(0.1f, 0.35f, 0.18f, 0.55f); statusLabel.style.display = DisplayStyle.Flex; }; void UpdateTriggerStatus(bool hasProp) { if (!hasProp) { statusLabel.style.display = DisplayStyle.None; return; } bool recentlyFired = lastTriggeredAt >= 0d && EditorApplication.timeSinceStartup - lastTriggeredAt <= TriggerFiredHighlightSeconds; statusLabel.style.display = recentlyFired ? DisplayStyle.Flex : DisplayStyle.None; } void DisposeSubscription() { if (subscribedProp != null) { subscribedProp.OnTriggered -= onTriggered; subscribedProp = null; } } m_triggerBindingDisposers.Add(DisposeSubscription); bindingList.Add(new PropertyBinding { Path = cacheKey, Control = button, Sync = instance => { var current = instanceProvider(); var prop = current?.GetTriggerProperty(path); button.SetEnabled(prop != null); if (!ReferenceEquals(subscribedProp, prop)) { DisposeSubscription(); subscribedProp = prop; if (subscribedProp != null) { subscribedProp.OnTriggered += onTriggered; } } UpdateTriggerStatus(prop != null); } }); var triggerControlRow = new VisualElement(); triggerControlRow.style.flexDirection = FlexDirection.Row; triggerControlRow.style.alignItems = Align.Center; triggerControlRow.Add(button); triggerControlRow.Add(statusLabel); parent.Add(CreatePropertyCard(displayName, pathLabelOverride ?? path, "Trigger", GetDocUrl(ViewModelDataType.Trigger), null, triggerControlRow)); } private void ClearTriggerBindings() { foreach (var dispose in m_triggerBindingDisposers) { dispose?.Invoke(); } m_triggerBindingDisposers.Clear(); } private void AddListField( VisualElement parent, string path, string displayName, Func instanceProvider = null, string cacheKey = null, string pathLabelOverride = null, List listBindingList = null, FileMetadata metadataContext = null, Func viewModelResolver = null) { instanceProvider ??= GetCurrentInstance; cacheKey ??= path; listBindingList ??= m_listBindings; metadataContext ??= m_fileMetadata; viewModelResolver ??= name => string.IsNullOrEmpty(name) ? null : m_widget?.File?.GetViewModelByName(name); var listContainer = new VisualElement(); listContainer.style.flexDirection = FlexDirection.Column; var vmNames = metadataContext?.ViewModels? .Select(vm => vm.Name) .Where(n => !string.IsNullOrEmpty(n)) .Distinct() .ToList() ?? new List(); var typeRow = new VisualElement { style = { flexDirection = FlexDirection.Row, alignItems = Align.Center } }; var initialType = vmNames.FirstOrDefault(); var typeDropdown = new DropdownField("New Item Type", vmNames, initialType) { tooltip = "Select the ViewModel type for newly added list items" }; typeDropdown.style.flexGrow = 1; typeDropdown.style.flexShrink = 1; typeDropdown.style.marginRight = 6; typeDropdown.style.marginBottom = 6; typeRow.Add(typeDropdown); typeRow.style.marginBottom = 4; listContainer.Add(typeRow); var itemBindings = new Dictionary>(); ViewModelInstance CreateInstanceForList() { string targetType = typeDropdown.value; var listProp = instanceProvider()?.GetListProperty(path); if (string.IsNullOrEmpty(targetType)) { targetType = listProp?.Count > 0 ? listProp.GetInstanceAt(0)?.ViewModelName : null; } var vm = !string.IsNullOrEmpty(targetType) ? viewModelResolver(targetType) : null; return vm?.CreateInstance(); } var listView = new ListView { reorderable = true, showAddRemoveFooter = true, virtualizationMethod = CollectionVirtualizationMethod.DynamicHeight, selectionType = SelectionType.Single }; listView.style.flexGrow = 1; listView.style.minHeight = 80; // Adapter that proxies directly to the live list property to avoid stale counts var listAdapter = new ListPropertyAdapter( () => instanceProvider()?.GetListProperty(path), CreateInstanceForList); listView.itemsSource = listAdapter; listView.makeItem = () => { var root = new VisualElement(); root.style.flexDirection = FlexDirection.Column; root.style.paddingTop = 6; root.style.paddingBottom = 6; root.style.paddingLeft = 4; root.style.paddingRight = 4; root.style.borderBottomWidth = 1; root.style.borderBottomColor = new UnityEngine.Color(0.25f, 0.25f, 0.25f, 0.25f); return root; }; listView.bindItem = (element, index) => { element.Clear(); var bindings = new List(); itemBindings[index] = bindings; var listProp = instanceProvider()?.GetListProperty(path); var instance = (listProp != null && index >= 0 && index < listProp.Count) ? listProp.GetInstanceAt(index) : null; var header = new VisualElement { style = { flexDirection = FlexDirection.Row, alignItems = Align.Center } }; var title = new Label(instance?.ViewModelName ?? "Item"); title.style.unityFontStyleAndWeight = FontStyle.Bold; title.style.flexGrow = 1; //header.Add(title); element.Add(header); if (instance == null) { element.Add(new HelpBox("List item is null.", HelpBoxMessageType.Warning)); return; } var meta = FindViewModelMetadata(instance.ViewModelName, metadataContext); if (meta == null) { element.Add(new HelpBox($"No metadata found for '{instance.ViewModelName}'.", HelpBoxMessageType.Warning)); return; } BuildViewModelSection(meta, string.Empty, element, 1, () => instance, $"{path}[{index}]", $"{path}[{index}]", bindings, null, metadataContext, viewModelResolver); }; listContainer.Add(listView); var listBinding = new ListPropertyBinding { Path = cacheKey, Control = listContainer, }; listBinding.Sync = _ => { var listProp = instanceProvider()?.GetListProperty(path); bool hasProp = listProp != null; listView.SetEnabled(hasProp); typeDropdown.SetEnabled(hasProp); if (!hasProp) { itemBindings.Clear(); listView.itemsSource = listAdapter; listView.Rebuild(); listView.ClearSelection(); listBinding.LastItems.Clear(); listBinding.InitializedTypeSelection = false; return; } // Detect structural changes to avoid unnecessary rebuilds that steal focus var currentItems = new List(listProp.Count); for (int i = 0; i < listProp.Count; i++) { currentItems.Add(listProp.GetInstanceAt(i)); } bool structureChanged = currentItems.Count != listBinding.LastItems.Count; if (!structureChanged) { for (int i = 0; i < currentItems.Count; i++) { if (!ReferenceEquals(currentItems[i], listBinding.LastItems[i])) { structureChanged = true; break; } } } if (structureChanged) { itemBindings.Clear(); listView.itemsSource = listAdapter; listView.Rebuild(); listView.ClearSelection(); listBinding.LastItems = currentItems; } else { listBinding.LastItems = currentItems; if (listView.selectedIndex >= listProp.Count) { listView.ClearSelection(); } } string inferred = listProp.Count > 0 ? listProp.GetInstanceAt(0)?.ViewModelName : null; if (!listBinding.InitializedTypeSelection) { if (!string.IsNullOrEmpty(inferred) && typeDropdown.choices.Contains(inferred)) { typeDropdown.SetValueWithoutNotify(inferred); } else if (string.IsNullOrEmpty(typeDropdown.value) && typeDropdown.choices.Count > 0) { typeDropdown.SetValueWithoutNotify(typeDropdown.choices[0]); } listBinding.InitializedTypeSelection = true; } else { bool selectionMissing = string.IsNullOrEmpty(typeDropdown.value) || !typeDropdown.choices.Contains(typeDropdown.value); if (selectionMissing && !string.IsNullOrEmpty(inferred) && typeDropdown.choices.Contains(inferred)) { typeDropdown.SetValueWithoutNotify(inferred); } } foreach (var kvp in itemBindings) { var idx = kvp.Key; if (idx < 0 || idx >= listProp.Count) { continue; } var instance = listProp.GetInstanceAt(idx); foreach (var binding in kvp.Value) { binding.Sync?.Invoke(instance); } } }; listBindingList.Add(listBinding); parent.Add(CreatePropertyCard(displayName, pathLabelOverride ?? path, "List", GetDocUrl(ViewModelDataType.List), null, listContainer, false)); } private void AddUnsupportedLabel(VisualElement parent, string name, ViewModelDataType type) { var label = new Label($"This property type is not configurable in the playground."); label.style.color = new UnityEngine.Color(0.7f, 0.7f, 0.7f); parent.Add(CreatePropertyCard(name, name, type.ToString(), GetDocUrl(type), null, label)); } private void RefreshValues() { if (m_isRefreshing) { return; } m_isRefreshing = true; var instance = GetCurrentInstance(); try { foreach (var binding in m_propertyBindings.ToList()) { binding.Sync?.Invoke(instance); } foreach (var listBinding in m_listBindings.ToList()) { listBinding.Sync?.Invoke(instance); } } finally { m_isRefreshing = false; } } private void UpdateVisibility() { string message; var state = GetPlaygroundState(out message); bool ready = state == PlaygroundState.Ready; if (m_playModeHelpBox != null) { m_playModeHelpBox.text = message; m_playModeHelpBox.style.display = ready ? DisplayStyle.None : DisplayStyle.Flex; } if (m_widgetField != null) { // Only show the widget selector while in Play Mode m_widgetField.style.display = EditorApplication.isPlaying ? DisplayStyle.Flex : DisplayStyle.None; } if (m_interactiveContainer != null) { m_interactiveContainer.style.display = ready ? DisplayStyle.Flex : DisplayStyle.None; } } private PlaygroundState GetPlaygroundState(out string message) { if (!EditorApplication.isPlaying) { message = "Enter Play Mode to update values in the widget."; return PlaygroundState.NotPlaying; } if (m_widget == null) { message = "Select a RiveWidget to get started."; return PlaygroundState.NoWidget; } if (m_fileMetadata == null) { message = "Selected Rive file has no metadata yet. Reimport or select a Rive asset."; return PlaygroundState.NoFileMetadata; } if (m_fileMetadata.ViewModels == null || m_fileMetadata.ViewModels.Count == 0) { message = "This Rive file has no ViewModels. Add data binding in the Rive Editor to use the playground."; return PlaygroundState.NoViewModels; } if (m_artboardMetadata == null) { message = "No artboard metadata found for the selected widget."; return PlaygroundState.NoArtboardMetadata; } if (m_artboardMetadata.DefaultViewModel == null) { message = "The current artboard has no default ViewModel."; return PlaygroundState.NoDefaultViewModel; } if (m_widget.Status != WidgetStatus.Loaded || m_widget.StateMachine == null) { message = "Widget is not loaded yet."; return PlaygroundState.WidgetNotLoaded; } if (m_widget.StateMachine.ViewModelInstance == null) { message = "No ViewModel instance bound to the state machine."; return PlaygroundState.NoViewModelInstance; } message = string.Empty; return PlaygroundState.Ready; } private Button CreateMoreActionsButton(string path, string displayName) { var button = new Button(() => { var menu = new GenericMenu(); bool hasIndexPath = !string.IsNullOrEmpty(path) && path.Contains("["); if (!string.IsNullOrEmpty(path) && !hasIndexPath) { menu.AddItem(new GUIContent("Copy Path"), false, () => { EditorGUIUtility.systemCopyBuffer = path; }); } else { string label = hasIndexPath ? "Copy Path (not supported for list items)" : "Copy Path"; menu.AddDisabledItem(new GUIContent(label)); } string nameOnly = !string.IsNullOrEmpty(path) ? path.Split('/').Last() : displayName; menu.AddItem(new GUIContent("Copy Name"), false, () => { EditorGUIUtility.systemCopyBuffer = nameOnly; }); menu.ShowAsContext(); }) { text = "⋮", tooltip = "More actions", }; button.style.width = 26; button.style.marginLeft = 4; button.style.height = 20; button.style.backgroundColor = new UnityEngine.Color(0, 0, 0, 0); button.style.borderLeftWidth = 0; button.style.borderRightWidth = 0; button.style.borderTopWidth = 0; button.style.borderBottomWidth = 0; button.style.fontSize = 14; button.style.unityFontStyleAndWeight = FontStyle.Bold; button.RegisterCallback(_ => { button.style.backgroundColor = new UnityEngine.Color(0.345f, 0.345f, 0.345f, 0.6f); }); button.RegisterCallback(_ => { button.style.backgroundColor = new UnityEngine.Color(0, 0, 0, 0); }); return button; } private void StyleLinkLabel(Label label, string url) { label.style.fontSize = 11; var linkColor = new UnityEngine.Color(0.55f, 0.78f, 1f, 1f); label.style.color = linkColor; label.style.marginLeft = 6; // We keep the border width constant to avoid layout shift; toggle only the color. label.style.borderBottomWidth = 1; label.style.borderBottomColor = new StyleColor(new UnityEngine.Color(0, 0, 0, 0)); label.RegisterCallback(_ => { label.style.borderBottomColor = new StyleColor(linkColor); }); label.RegisterCallback(_ => { label.style.borderBottomColor = new StyleColor(new UnityEngine.Color(0, 0, 0, 0)); }); label.RegisterCallback(_ => Application.OpenURL(url)); } private ViewModelInstance GetCurrentInstance() { return m_widget?.StateMachine?.ViewModelInstance; } private void BuildViewModelSection( FileMetadata.ViewModelMetadata viewModel, string accessPathPrefix, VisualElement parent, int depth, Func instanceProvider = null, string displayPathPrefix = null, string cachePathPrefix = null, List bindingList = null, List listBindingList = null, FileMetadata metadataContext = null, Func viewModelResolver = null) { instanceProvider ??= GetCurrentInstance; bindingList ??= m_propertyBindings; listBindingList ??= m_listBindings; metadataContext ??= m_fileMetadata; viewModelResolver ??= name => string.IsNullOrEmpty(name) ? null : m_widget?.File?.GetViewModelByName(name); string resolvedDisplayPrefix = displayPathPrefix ?? accessPathPrefix; string resolvedCachePrefix = cachePathPrefix ?? accessPathPrefix; string expansionKey = string.IsNullOrEmpty(resolvedCachePrefix) ? "(root)" : resolvedCachePrefix; string vmLabel = string.IsNullOrEmpty(accessPathPrefix) ? (string.IsNullOrEmpty(viewModel.Name) ? "Default ViewModel" : viewModel.Name) : accessPathPrefix.Split('/').Last(); string viewModelNameLabel = string.IsNullOrEmpty(viewModel.Name) ? null : viewModel.Name; string pathLabel = string.IsNullOrEmpty(resolvedDisplayPrefix) ? "(root)" : resolvedDisplayPrefix; int childCount = viewModel.Properties?.Count ?? 0; bool expanded = m_viewModelExpansion.TryGetValue(expansionKey, out var savedExpanded) ? savedExpanded : depth == 0; var header = new VisualElement { style = { flexDirection = FlexDirection.Row, alignItems = Align.Center } }; var expander = new Button(); expander.style.width = 20; expander.style.height = 20; expander.style.paddingLeft = 0; expander.style.paddingRight = 0; expander.style.marginRight = 3; expander.text = expanded ? "▾" : "▸"; expander.tooltip = "Expand / collapse view model"; expander.style.fontSize = 30; expander.style.backgroundColor = new UnityEngine.Color(0, 0, 0, 0); expander.style.borderLeftWidth = 0; expander.style.borderRightWidth = 0; expander.style.borderTopWidth = 0; expander.style.borderBottomWidth = 0; var vmName = new Label(vmLabel) { style = { unityFontStyleAndWeight = FontStyle.Bold, flexGrow = 1 } }; var vmPill = new Label(string.IsNullOrEmpty(viewModelNameLabel) ? "View Model" : $"View Model ({viewModelNameLabel})"); vmPill.style.unityTextAlign = TextAnchor.MiddleCenter; vmPill.style.paddingLeft = 6; vmPill.style.paddingRight = 6; vmPill.style.paddingTop = 2; vmPill.style.paddingBottom = 2; vmPill.style.marginLeft = 4; vmPill.style.borderTopLeftRadius = 4; vmPill.style.borderTopRightRadius = 4; vmPill.style.borderBottomLeftRadius = 4; vmPill.style.borderBottomRightRadius = 4; vmPill.style.backgroundColor = new UnityEngine.Color(0.25f, 0.25f, 0.35f, 0.9f); vmPill.style.color = new UnityEngine.Color(0.9f, 0.9f, 1f, 1f); var countLabel = new Label($"{childCount} properties"); countLabel.style.marginLeft = 6; countLabel.style.fontSize = 11; countLabel.style.color = new UnityEngine.Color(0.8f, 0.8f, 0.85f, 0.9f); header.Add(expander); header.Add(vmName); header.Add(vmPill); header.Add(countLabel); header.Add(CreateMoreActionsButton(resolvedDisplayPrefix, vmLabel)); var container = new VisualElement(); container.style.marginLeft = 12; container.style.marginTop = 8; container.style.marginBottom = 8; container.style.display = expanded ? DisplayStyle.Flex : DisplayStyle.None; container.style.flexDirection = FlexDirection.Column; foreach (var property in viewModel.Properties) { string propertyAccessPath = string.IsNullOrEmpty(accessPathPrefix) ? property.Name : $"{accessPathPrefix}/{property.Name}"; string propertyDisplayPath = string.IsNullOrEmpty(resolvedDisplayPrefix) ? property.Name : $"{resolvedDisplayPrefix}/{property.Name}"; string propertyCachePath = string.IsNullOrEmpty(resolvedCachePrefix) ? propertyAccessPath : $"{resolvedCachePrefix}/{property.Name}"; switch (property.Type) { case ViewModelDataType.String: AddStringField(container, propertyAccessPath, property.Name, instanceProvider, propertyCachePath, propertyDisplayPath, bindingList); break; case ViewModelDataType.Number: AddNumberField(container, propertyAccessPath, property.Name, instanceProvider, propertyCachePath, propertyDisplayPath, bindingList); break; case ViewModelDataType.Boolean: AddBooleanField(container, propertyAccessPath, property.Name, instanceProvider, propertyCachePath, propertyDisplayPath, bindingList); break; case ViewModelDataType.Color: AddColorField(container, propertyAccessPath, property.Name, instanceProvider, propertyCachePath, propertyDisplayPath, bindingList); break; case ViewModelDataType.AssetImage: AddImageField(container, propertyAccessPath, property.Name, instanceProvider, propertyCachePath, propertyDisplayPath, bindingList); break; case ViewModelDataType.Artboard: AddArtboardField(container, propertyAccessPath, property.Name, instanceProvider, propertyCachePath, propertyDisplayPath, bindingList); break; case ViewModelDataType.Enum: AddEnumField(container, propertyAccessPath, property, instanceProvider, propertyCachePath, propertyDisplayPath, bindingList, metadataContext); break; case ViewModelDataType.Trigger: AddTriggerField(container, propertyAccessPath, property.Name, instanceProvider, propertyCachePath, propertyDisplayPath, bindingList); break; case ViewModelDataType.ViewModel: var nestedMeta = FindViewModelMetadata(property.NestedViewModelName, metadataContext); if (nestedMeta != null) { BuildViewModelSection(nestedMeta, propertyAccessPath, container, depth + 1, instanceProvider, propertyDisplayPath, propertyCachePath, bindingList, listBindingList, metadataContext, viewModelResolver); } else { var missingLabel = new Label($"Nested view model '{property.NestedViewModelName}' not found."); missingLabel.style.color = UnityEngine.Color.yellow; container.Add(missingLabel); } break; case ViewModelDataType.List: AddListField(container, propertyAccessPath, property.Name, instanceProvider, propertyCachePath, propertyDisplayPath, listBindingList, metadataContext, viewModelResolver); break; case ViewModelDataType.ListIndex: AddUnsupportedLabel(container, property.Name, property.Type); break; default: AddUnsupportedLabel(container, property.Name, property.Type); break; } } void ToggleExpanded() { expanded = !expanded; container.style.display = expanded ? DisplayStyle.Flex : DisplayStyle.None; expander.text = expanded ? "▾" : "▸"; m_viewModelExpansion[expansionKey] = expanded; } expander.clicked += ToggleExpanded; // wrap in a card for clarity var vmCard = new VisualElement(); vmCard.style.marginTop = depth == 0 ? 8 : 4; vmCard.style.paddingTop = 8; vmCard.style.paddingBottom = 8; vmCard.style.paddingLeft = 8; vmCard.style.paddingRight = 8; vmCard.style.borderTopWidth = 1; vmCard.style.borderBottomWidth = 1; vmCard.style.borderLeftWidth = 1; vmCard.style.borderRightWidth = 1; vmCard.style.borderTopColor = new UnityEngine.Color(0.344f, 0.344f, 0.349f, 0.5f); vmCard.style.borderBottomColor = new UnityEngine.Color(0.344f, 0.344f, 0.349f, 0.5f); vmCard.style.borderLeftColor = new UnityEngine.Color(0.344f, 0.344f, 0.349f, 0.5f); vmCard.style.borderRightColor = new UnityEngine.Color(0.344f, 0.344f, 0.349f, 0.5f); vmCard.style.backgroundColor = new UnityEngine.Color(0.145f, 0.145f, 0.152f, 0.55f); vmCard.style.borderTopLeftRadius = 6; vmCard.style.borderTopRightRadius = 6; vmCard.style.borderBottomLeftRadius = 6; vmCard.style.borderBottomRightRadius = 6; vmCard.Add(header); vmCard.Add(container); parent.Add(vmCard); } private string GetDocUrl(ViewModelDataType type) { switch (type) { case ViewModelDataType.String: return InspectorDocLinks.UnityDataBindingProperties; case ViewModelDataType.Number: return InspectorDocLinks.UnityDataBindingProperties; case ViewModelDataType.Boolean: return InspectorDocLinks.UnityDataBindingProperties; case ViewModelDataType.Color: return InspectorDocLinks.UnityDataBindingProperties; case ViewModelDataType.Trigger: return InspectorDocLinks.UnityDataBindingProperties; case ViewModelDataType.Enum: return InspectorDocLinks.UnityDataBindingEnums; case ViewModelDataType.ViewModel: return InspectorDocLinks.UnityDataBindingViewModel; case ViewModelDataType.AssetImage: return InspectorDocLinks.UnityDataBindingImages; case ViewModelDataType.List: return InspectorDocLinks.UnityDataBindingLists; case ViewModelDataType.ListIndex: return InspectorDocLinks.UnityDataBindingListViewModelIndex; case ViewModelDataType.Artboard: return InspectorDocLinks.UnityDataBindingArtboards; default: return InspectorDocLinks.UnityDataBinding; } } private VisualElement CreatePropertyCard(string displayName, string path, string typeLabel, string docUrl, string description, VisualElement control, bool showTypePill = false) { var card = new VisualElement(); card.style.marginTop = 6; card.style.paddingTop = 8; card.style.paddingBottom = 8; card.style.paddingLeft = 10; card.style.paddingRight = 10; card.style.borderTopLeftRadius = 6; card.style.borderTopRightRadius = 6; card.style.borderBottomLeftRadius = 6; card.style.borderBottomRightRadius = 6; card.style.borderBottomWidth = 1; card.style.borderTopWidth = 1; card.style.borderLeftWidth = 1; card.style.borderRightWidth = 1; card.style.borderBottomColor = new UnityEngine.Color(0.25f, 0.25f, 0.25f, 0.4f); card.style.borderTopColor = new UnityEngine.Color(0.25f, 0.25f, 0.25f, 0.4f); card.style.borderLeftColor = new UnityEngine.Color(0.25f, 0.25f, 0.25f, 0.4f); card.style.borderRightColor = new UnityEngine.Color(0.25f, 0.25f, 0.25f, 0.4f); card.style.backgroundColor = new UnityEngine.Color(0.12f, 0.12f, 0.12f, 0.45f); var header = new VisualElement { style = { flexDirection = FlexDirection.Row, alignItems = Align.Center } }; var nameLabel = new Label(displayName); nameLabel.style.unityFontStyleAndWeight = FontStyle.Bold; nameLabel.style.flexGrow = 1; if (showTypePill && !string.IsNullOrEmpty(typeLabel)) { var typePill = new Label(typeLabel); typePill.style.unityTextAlign = TextAnchor.MiddleCenter; typePill.style.paddingLeft = 6; typePill.style.paddingRight = 6; typePill.style.paddingTop = 2; typePill.style.paddingBottom = 2; typePill.style.marginLeft = 4; typePill.style.borderTopLeftRadius = 4; typePill.style.borderTopRightRadius = 4; typePill.style.borderBottomLeftRadius = 4; typePill.style.borderBottomRightRadius = 4; typePill.style.backgroundColor = new UnityEngine.Color(0.25f, 0.25f, 0.25f, 0.8f); typePill.style.color = new UnityEngine.Color(0.85f, 0.85f, 0.85f, 1f); header.Add(typePill); } var menuButton = CreateMoreActionsButton(path, displayName); // Remove potential parenthesis text to account for enum values. E.g. "Enum (MyEnum)" should be "Enum" string strippedTypeLabel = typeLabel; if (!string.IsNullOrEmpty(strippedTypeLabel)) { int parenIndex = strippedTypeLabel.IndexOf(" (", StringComparison.Ordinal); if (parenIndex > 0) { strippedTypeLabel = strippedTypeLabel.Substring(0, parenIndex); } } string docLabel = string.IsNullOrEmpty(typeLabel) ? "Docs" : $"{strippedTypeLabel} Property Documentation"; var docButton = new Label(docLabel); StyleLinkLabel(docButton, docUrl); docButton.style.marginLeft = 6; docButton.style.alignSelf = Align.Center; docButton.style.display = DisplayStyle.None; header.Add(nameLabel); header.Add(docButton); header.Add(menuButton); card.Add(header); card.RegisterCallback(_ => { docButton.style.display = DisplayStyle.Flex; }); card.RegisterCallback(_ => { docButton.style.display = DisplayStyle.None; }); string pathLineText = null; if (!showTypePill) { pathLineText = $"{typeLabel}"; } else if (!string.IsNullOrEmpty(path)) { pathLineText = $"Path: {path}"; } if (!string.IsNullOrEmpty(pathLineText)) { var pathLabel = new Label(pathLineText); pathLabel.style.fontSize = 11; pathLabel.style.color = new UnityEngine.Color(0.75f, 0.75f, 0.75f, 0.9f); pathLabel.style.marginTop = 2; card.Add(pathLabel); } if (!string.IsNullOrEmpty(description)) { var desc = new Label(description); desc.style.fontSize = 11; desc.style.color = new UnityEngine.Color(0.8f, 0.8f, 0.8f, 0.95f); desc.style.marginTop = 4; card.Add(desc); } control.style.marginTop = 6; card.Add(control); return card; } } }