diff --git a/org.mixedrealitytoolkit.uxcore/CHANGELOG.md b/org.mixedrealitytoolkit.uxcore/CHANGELOG.md index f5b02e2d5..3505c0b10 100644 --- a/org.mixedrealitytoolkit.uxcore/CHANGELOG.md +++ b/org.mixedrealitytoolkit.uxcore/CHANGELOG.md @@ -8,10 +8,15 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). * Added `AlphaBlend` tint mode to `TintEffect`. [PR #1131](https://github.com/MixedRealityToolkit/MixedRealityToolkit-Unity/pull/1131) +### Changed + +* Removed redundant `Update()` from `CanvasSliderVisuals` in favor of event-driven updates and `OnValidate()`. [PR #1144](https://github.com/MixedRealityToolkit/MixedRealityToolkit-Unity/pull/1144) + ### Fixed * Fixed "leaked managed shell" issue in `UGUIInputAdapter`. [PR #1096](https://github.com/MixedRealityToolkit/MixedRealityToolkit-Unity/pull/1096) * Fixed "Attribute 'SerializeField' is not valid on this declaration type. It is only valid on 'field' declarations" error on `DialogButton` in Unity 6.3. [PR #1108](https://github.com/MixedRealityToolkit/MixedRealityToolkit-Unity/pull/1108) +* Fixed slider handle visuals not updating when slider value is modified from Inspector in play mode. [PR #1144](https://github.com/MixedRealityToolkit/MixedRealityToolkit-Unity/pull/1144) ## [3.3.0] - 2025-11-12 diff --git a/org.mixedrealitytoolkit.uxcore/Slider/Slider.cs b/org.mixedrealitytoolkit.uxcore/Slider/Slider.cs index c0bc6b755..905827e4d 100644 --- a/org.mixedrealitytoolkit.uxcore/Slider/Slider.cs +++ b/org.mixedrealitytoolkit.uxcore/Slider/Slider.cs @@ -1,4 +1,4 @@ -// Copyright (c) Mixed Reality Toolkit Contributors +// Copyright (c) Mixed Reality Toolkit Contributors // Licensed under the BSD 3-Clause using System; @@ -103,14 +103,8 @@ public Collider TrackCollider /// public float MinValue { - get - { - return minValue; - } - set - { - minValue = Mathf.Min(value, maxValue); - } + get => minValue; + set => minValue = Mathf.Min(value, maxValue); } [SerializeField] @@ -122,14 +116,8 @@ public float MinValue /// public float MaxValue { - get - { - return maxValue; - } - set - { - maxValue = Mathf.Max(minValue, value); - } + get => maxValue; + set => maxValue = Mathf.Max(minValue, value); } [VariableRange("minValue", "maxValue")] @@ -149,16 +137,7 @@ public float MaxValue public float Value { get => value; - set - { - if (this.value != value) - { - value = Mathf.Clamp(value, minValue, maxValue); - var oldSliderValue = this.value; - this.value = value; - OnValueUpdated.Invoke(new SliderEventData(oldSliderValue, value)); - } - } + set => UpdateValue(value); } /// @@ -176,7 +155,14 @@ public float Value public bool UseSliderStepDivisions { get => useSliderStepDivisions; - set => useSliderStepDivisions = value; + set + { + useSliderStepDivisions = value; + if (useSliderStepDivisions) + { + UpdateValue(this.value); + } + } } [SerializeField] @@ -190,7 +176,14 @@ public bool UseSliderStepDivisions public int SliderStepDivisions { get => sliderStepDivisions; - set => sliderStepDivisions = value; + set + { + sliderStepDivisions = value; + if (useSliderStepDivisions) + { + UpdateValue(this.value); + } + } } [Header("Layout")] @@ -240,6 +233,7 @@ public Transform SliderEnd /// A Unity event that is invoked when changes. /// public SliderEvent OnValueUpdated => onValueUpdated; + #endregion #region Private Fields @@ -285,7 +279,7 @@ protected override void Reset() /// /// A Unity event function that is called on the frame when a script is enabled just before any of the update methods are called the first time. - /// + /// protected virtual void Start() { // Turn on/off colliders at Start() to avoid bugs with @@ -324,14 +318,15 @@ private void OnValidate() // Ensure that the proper constraints are applied to the possible values of the slider MinValue = minValue; MaxValue = maxValue; - Value = value; + UpdateValue(value, forceUpdate: true); } #endregion #region Protected Methods + /// - /// Invoked on , , and to apply required + /// Invoked on , , and to apply required /// settings to this instance. /// /// @@ -344,22 +339,45 @@ protected virtual void ApplyRequiredSettings() // been acquired. selectMode = InteractableSelectMode.Single; } + #endregion Protected Methods #region Private Methods - /// + + /// /// Private method used to adjust initial slider value to stepwise values /// private void InitializeStepDivisions() { - Value = SnapSliderToStepPositions(Value); + UpdateValue(value); + } + + private float ClampAndSnapValue(float rawValue) + { + float clamped = Mathf.Clamp(rawValue, minValue, maxValue); + if (useSliderStepDivisions) + { + clamped = SnapSliderToStepPositions(clamped); + } + return Mathf.Clamp(clamped, minValue, maxValue); + } + + private void UpdateValue(float newValue, bool forceUpdate = false) + { + float clampedValue = ClampAndSnapValue(newValue); + + if (forceUpdate || this.value != clampedValue) + { + var oldSliderValue = this.value; + this.value = clampedValue; + OnValueUpdated.Invoke(new SliderEventData(oldSliderValue, clampedValue)); + } } private float SnapSliderToStepPositions(float value) { var stepCount = value / SliderStepVal; var snappedValue = SliderStepVal * Mathf.RoundToInt(stepCount); - Mathf.Clamp(snappedValue, 0f, 1.0f); return snappedValue; } @@ -373,8 +391,7 @@ private void UpdateSliderValue() var normalizedStartValue = (StartSliderValue - MinValue) / (MaxValue - MinValue); float normalizedValue = Mathf.Clamp(normalizedStartValue + handDelta / SliderTrackDirection.magnitude, 0f, 1.0f); - var unsnappedValue = normalizedValue * (MaxValue - MinValue) + MinValue; - Value = useSliderStepDivisions ? SnapSliderToStepPositions(unsnappedValue) : unsnappedValue; + UpdateValue(normalizedValue * (MaxValue - MinValue) + MinValue); } #endregion diff --git a/org.mixedrealitytoolkit.uxcore/Slider/Visuals/CanvasSliderVisuals.cs b/org.mixedrealitytoolkit.uxcore/Slider/Visuals/CanvasSliderVisuals.cs index 9eb29634f..ad95a3c8f 100644 --- a/org.mixedrealitytoolkit.uxcore/Slider/Visuals/CanvasSliderVisuals.cs +++ b/org.mixedrealitytoolkit.uxcore/Slider/Visuals/CanvasSliderVisuals.cs @@ -130,9 +130,13 @@ public Direction SliderDirection /// private void OnEnable() { - sliderStart = SliderState.SliderStart.EnsureComponent(); - sliderEnd = SliderState.SliderEnd.EnsureComponent(); - touchableFitter = TrackArea.GetComponent(); + EnsureSliderEndpoints(); + + if (TrackArea != null) + { + touchableFitter = TrackArea.GetComponent(); + } + uguiInputAdapter = GetComponent(); if (sliderStart == null || sliderEnd == null) @@ -140,10 +144,14 @@ private void OnEnable() Debug.LogError("Slider is missing start/end transforms."); } - SliderState.OnValueUpdated.AddListener(UpdateHandle); + if (SliderState != null) + { + SliderState.OnValueUpdated.AddListener(UpdateHandle); + + // Initial update. We may miss the first OnValueUpdated, depending on execution order. + UpdateHandle(SliderState.NormalizedValue); + } - // Initial update. We may miss the first OnValueUpdated, depending on execution order. - UpdateHandle(SliderState.NormalizedValue); SetLayout(sliderDirection); } @@ -156,17 +164,22 @@ private void OnDisable() } #if UNITY_EDITOR - // Keep track of the last direction, so we can re-layout if needed. private Direction prevDirection; /// - /// A Unity event function that is called every frame, if this object is enabled. + /// A Unity Editor only event function that is called when the script is loaded or a value changes in the Unity Inspector. /// - private void Update() + private void OnValidate() { - // Only do this in edit mode, for performance. - if (Application.isPlaying) + if (trackArea == null || handle == null || fillVisual == null) + { + return; + } + + EnsureSliderEndpoints(); + + if (sliderStart == null || sliderEnd == null) { return; } @@ -179,14 +192,17 @@ private void Update() trackArea.anchoredPosition = Vector3.zero; // Helper to apply layout if set from inspector. - if (SliderDirection != prevDirection) + if (sliderDirection != prevDirection) { - SetLayout(SliderDirection); - prevDirection = SliderDirection; + SetLayout(sliderDirection); + prevDirection = sliderDirection; } // Update handle in editor view. - UpdateHandle(SliderState.NormalizedValue); + if (SliderState != null) + { + UpdateHandle(SliderState.NormalizedValue); + } } #endif // UNITY_EDITOR @@ -199,6 +215,11 @@ void UpdateHandle(SliderEventData data) // Update the things that depend on the slider value. void UpdateHandle(float value) { + if (handle == null || fillVisual == null) + { + return; + } + switch (SliderDirection) { case Direction.LeftToRight: @@ -225,17 +246,47 @@ void UpdateHandle(float value) handle.anchoredPosition = Vector3.zero; } + private void EnsureSliderEndpoints() + { + if (sliderStart == null && SliderState != null && SliderState.SliderStart != null) + { + sliderStart = Application.isPlaying + ? SliderState.SliderStart.EnsureComponent() + : SliderState.SliderStart.GetComponent(); + } + + if (sliderEnd == null && SliderState != null && SliderState.SliderEnd != null) + { + sliderEnd = Application.isPlaying + ? SliderState.SliderEnd.EnsureComponent() + : SliderState.SliderEnd.GetComponent(); + } + } + void SetLayout(Direction direction) { + // Ensure sliderStart and sliderEnd are rooted at 0, we change the positions of these points via their anchors + sliderStart.anchoredPosition = Vector3.zero; + sliderEnd.anchoredPosition = Vector3.zero; + + if (uguiInputAdapter == null) + { + uguiInputAdapter = GetComponent(); + } + + // Make sure UGUI understands which axis we can + // slide on. (Affects dpad/gamepad/etc) + if (uguiInputAdapter != null) + { + uguiInputAdapter.MovableAxes = (direction == Direction.LeftToRight || direction == Direction.RightToLeft) + ? AxisFlags.XAxis + : AxisFlags.YAxis; + } + float trackWidth = Mathf.Max(trackArea.sizeDelta.x, trackArea.sizeDelta.y); switch (direction) { case Direction.LeftToRight: - - // Make sure UGUI understands which axis we can - // slide on. (Affects dpad/gamepad/etc) - uguiInputAdapter.MovableAxes = AxisFlags.XAxis; - sliderStart.anchorMin = new Vector2(0.0f, 0.5f); sliderStart.anchorMax = new Vector2(0.0f, 0.5f); sliderEnd.anchorMin = new Vector2(1.0f, 0.5f); @@ -251,11 +302,6 @@ void SetLayout(Direction direction) break; case Direction.RightToLeft: - - // Make sure UGUI understands which axis we can - // slide on. (Affects dpad/gamepad/etc) - uguiInputAdapter.MovableAxes = AxisFlags.XAxis; - sliderStart.anchorMin = new Vector2(1.0f, 0.5f); sliderStart.anchorMax = new Vector2(1.0f, 0.5f); sliderEnd.anchorMin = new Vector2(0.0f, 0.5f); @@ -271,11 +317,6 @@ void SetLayout(Direction direction) break; case Direction.BottomToTop: - - // Make sure UGUI understands which axis we can - // slide on. (Affects dpad/gamepad/etc) - uguiInputAdapter.MovableAxes = AxisFlags.YAxis; - sliderStart.anchorMin = new Vector2(0.5f, 0.0f); sliderStart.anchorMax = new Vector2(0.5f, 0.0f); sliderEnd.anchorMin = new Vector2(0.5f, 1.0f); @@ -291,11 +332,6 @@ void SetLayout(Direction direction) break; case Direction.TopToBottom: - - // Make sure UGUI understands which axis we can - // slide on. (Affects dpad/gamepad/etc) - uguiInputAdapter.MovableAxes = AxisFlags.YAxis; - sliderStart.anchorMin = new Vector2(0.5f, 1); sliderStart.anchorMax = new Vector2(0.5f, 1); sliderEnd.anchorMin = new Vector2(0.5f, 0); diff --git a/org.mixedrealitytoolkit.uxcore/Tests/Editor/CanvasSliderVisualsTests.cs b/org.mixedrealitytoolkit.uxcore/Tests/Editor/CanvasSliderVisualsTests.cs new file mode 100644 index 000000000..baf884879 --- /dev/null +++ b/org.mixedrealitytoolkit.uxcore/Tests/Editor/CanvasSliderVisualsTests.cs @@ -0,0 +1,209 @@ +// Copyright (c) Mixed Reality Toolkit Contributors +// Licensed under the BSD 3-Clause + +// Disable "missing XML comment" warning for tests. While nice to have, this documentation is not required. +#pragma warning disable CS1591 + +using MixedReality.Toolkit.UX; +using NUnit.Framework; +using System.Reflection; +using UnityEditor; +using UnityEngine; + +namespace MixedReality.Toolkit.UX.Tests.EditMode +{ + public class CanvasSliderVisualsTests + { + private GameObject sliderObject; + private Slider slider; + private CanvasSliderVisuals visuals; + private RectTransform sliderStart; + private RectTransform sliderEnd; + private RectTransform trackArea; + private RectTransform handle; + private RectTransform fillVisual; + + [SetUp] + public void Setup() + { + sliderObject = new GameObject("TestSlider", typeof(RectTransform), typeof(Slider)); + sliderObject.SetActive(false); + slider = sliderObject.GetComponent(); + + GameObject startGo = new GameObject("SliderStart", typeof(RectTransform)); + startGo.transform.SetParent(sliderObject.transform, false); + sliderStart = startGo.GetComponent(); + + GameObject endGo = new GameObject("SliderEnd", typeof(RectTransform)); + endGo.transform.SetParent(sliderObject.transform, false); + sliderEnd = endGo.GetComponent(); + + GameObject trackGo = new GameObject("TrackArea", typeof(RectTransform)); + trackGo.transform.SetParent(sliderObject.transform, false); + trackArea = trackGo.GetComponent(); + trackArea.sizeDelta = new Vector2(100f, 10f); + + GameObject fillGo = new GameObject("FillVisual", typeof(RectTransform)); + fillGo.transform.SetParent(trackGo.transform, false); + fillVisual = fillGo.GetComponent(); + + GameObject handleGo = new GameObject("Handle", typeof(RectTransform)); + handleGo.transform.SetParent(trackGo.transform, false); + handle = handleGo.GetComponent(); + + slider.SliderStart = sliderStart; + slider.SliderEnd = sliderEnd; + slider.MinValue = 0f; + slider.MaxValue = 1f; + slider.Value = 0f; + + visuals = sliderObject.AddComponent(); + visuals.TrackArea = trackArea; + visuals.Handle = handle; + visuals.FillVisual = fillVisual; + sliderObject.SetActive(true); + } + + [TearDown] + public void TearDown() + { + if (sliderObject != null) + { + Object.DestroyImmediate(sliderObject); + } + } + + private static void InvokeOnValidate(Object target) + { + MethodInfo onValidate = target.GetType().GetMethod("OnValidate", BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public); + if (onValidate != null) + { + onValidate.Invoke(target, null); + } + } + + [Test] + public void Slider_OnValidate_ClampsAndSnapsValue_AndFiresOnValueUpdated() + { + slider.MinValue = 0f; + slider.MaxValue = 10f; + slider.UseSliderStepDivisions = true; + slider.SliderStepDivisions = 10; + + // Set raw serialized value to out-of-range un-snapped value + SerializedObject so = new SerializedObject(slider); + so.FindProperty("value").floatValue = 12.3f; + so.ApplyModifiedProperties(); + + bool eventFired = false; + float reportedValue = -1f; + slider.OnValueUpdated.AddListener(data => + { + eventFired = true; + reportedValue = data.NewValue; + }); + + InvokeOnValidate(slider); + + Assert.IsTrue(eventFired, "OnValueUpdated should have been fired by Slider.OnValidate()"); + Assert.AreEqual(10f, slider.Value, 0.0001f, "Slider.Value should be clamped to MaxValue"); + Assert.AreEqual(10f, reportedValue, 0.0001f, "SliderEventData should report the clamped value"); + } + + [Test] + public void CanvasSliderVisuals_EditMode_RespondsToSliderValueUpdate() + { + // Because CanvasSliderVisuals is [ExecuteAlways], in EditMode it listens to OnValueUpdated. + slider.Value = 0.75f; + + Assert.AreEqual(0.75f, handle.anchorMin.x, 0.001f, "Handle anchorMin.x should update to reflect 0.75"); + Assert.AreEqual(0.75f, handle.anchorMax.x, 0.001f, "Handle anchorMax.x should update to reflect 0.75"); + Assert.AreEqual(0.75f, fillVisual.anchorMax.x, 0.001f, "FillVisual anchorMax.x should update to reflect 0.75"); + } + + [Test] + public void CanvasSliderVisuals_EditMode_SliderOnValidateSyncsVisuals() + { + // Simulate changing slider value in inspector during Edit Mode + SerializedObject so = new SerializedObject(slider); + so.FindProperty("value").floatValue = 0.4f; + so.ApplyModifiedProperties(); + + InvokeOnValidate(slider); + + Assert.AreEqual(0.4f, handle.anchorMin.x, 0.001f, "Handle anchorMin.x should update when Slider.OnValidate runs"); + Assert.AreEqual(0.4f, fillVisual.anchorMax.x, 0.001f, "FillVisual anchorMax.x should update when Slider.OnValidate runs"); + } + + [Test] + public void CanvasSliderVisuals_SliderDirectionProperty_AppliesLayoutAndHandlePosition() + { + slider.Value = 0.5f; + + // Test RightToLeft + visuals.SliderDirection = CanvasSliderVisuals.Direction.RightToLeft; + Assert.AreEqual(new Vector2(1f, 0.5f), sliderStart.anchorMin); + Assert.AreEqual(new Vector2(0f, 0.5f), sliderEnd.anchorMin); + Assert.AreEqual(0.5f, handle.anchorMin.x, 0.001f); + + // Test BottomToTop + visuals.SliderDirection = CanvasSliderVisuals.Direction.BottomToTop; + Assert.AreEqual(new Vector2(0.5f, 0f), sliderStart.anchorMin); + Assert.AreEqual(new Vector2(0.5f, 1f), sliderEnd.anchorMin); + Assert.AreEqual(0.5f, handle.anchorMin.y, 0.001f); + + // Test TopToBottom + visuals.SliderDirection = CanvasSliderVisuals.Direction.TopToBottom; + Assert.AreEqual(new Vector2(0.5f, 1f), sliderStart.anchorMin); + Assert.AreEqual(new Vector2(0.5f, 0f), sliderEnd.anchorMin); + Assert.AreEqual(0.5f, handle.anchorMin.y, 0.001f); + + // Test LeftToRight + visuals.SliderDirection = CanvasSliderVisuals.Direction.LeftToRight; + Assert.AreEqual(new Vector2(0f, 0.5f), sliderStart.anchorMin); + Assert.AreEqual(new Vector2(1f, 0.5f), sliderEnd.anchorMin); + Assert.AreEqual(0.5f, handle.anchorMin.x, 0.001f); + } + + [Test] + public void CanvasSliderVisuals_OnValidate_AppliesLayoutAndHandlePosition() + { + slider.Value = 0.5f; + SerializedObject so = new SerializedObject(visuals); + SerializedProperty dirProp = so.FindProperty("sliderDirection"); + + // Test RightToLeft + dirProp.enumValueIndex = (int)CanvasSliderVisuals.Direction.RightToLeft; + so.ApplyModifiedProperties(); + InvokeOnValidate(visuals); + Assert.AreEqual(new Vector2(1f, 0.5f), sliderStart.anchorMin); + Assert.AreEqual(new Vector2(0f, 0.5f), sliderEnd.anchorMin); + Assert.AreEqual(0.5f, handle.anchorMin.x, 0.001f); + + // Test BottomToTop + dirProp.enumValueIndex = (int)CanvasSliderVisuals.Direction.BottomToTop; + so.ApplyModifiedProperties(); + InvokeOnValidate(visuals); + Assert.AreEqual(new Vector2(0.5f, 0f), sliderStart.anchorMin); + Assert.AreEqual(new Vector2(0.5f, 1f), sliderEnd.anchorMin); + Assert.AreEqual(0.5f, handle.anchorMin.y, 0.001f); + + // Test TopToBottom + dirProp.enumValueIndex = (int)CanvasSliderVisuals.Direction.TopToBottom; + so.ApplyModifiedProperties(); + InvokeOnValidate(visuals); + Assert.AreEqual(new Vector2(0.5f, 1f), sliderStart.anchorMin); + Assert.AreEqual(new Vector2(0.5f, 0f), sliderEnd.anchorMin); + Assert.AreEqual(0.5f, handle.anchorMin.y, 0.001f); + + // Test LeftToRight + dirProp.enumValueIndex = (int)CanvasSliderVisuals.Direction.LeftToRight; + so.ApplyModifiedProperties(); + InvokeOnValidate(visuals); + Assert.AreEqual(new Vector2(0f, 0.5f), sliderStart.anchorMin); + Assert.AreEqual(new Vector2(1f, 0.5f), sliderEnd.anchorMin); + Assert.AreEqual(0.5f, handle.anchorMin.x, 0.001f); + } + } +} +#pragma warning restore CS1591 diff --git a/org.mixedrealitytoolkit.uxcore/Tests/Editor/CanvasSliderVisualsTests.cs.meta b/org.mixedrealitytoolkit.uxcore/Tests/Editor/CanvasSliderVisualsTests.cs.meta new file mode 100644 index 000000000..6c2f08ade --- /dev/null +++ b/org.mixedrealitytoolkit.uxcore/Tests/Editor/CanvasSliderVisualsTests.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 64e0c647ae5de7c43968c831c636d807 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/org.mixedrealitytoolkit.uxcore/Tests/Editor/MRTK.UXCore.EditorTests.asmdef b/org.mixedrealitytoolkit.uxcore/Tests/Editor/MRTK.UXCore.EditorTests.asmdef index 77be8997b..82c9d0677 100644 --- a/org.mixedrealitytoolkit.uxcore/Tests/Editor/MRTK.UXCore.EditorTests.asmdef +++ b/org.mixedrealitytoolkit.uxcore/Tests/Editor/MRTK.UXCore.EditorTests.asmdef @@ -2,8 +2,11 @@ "name": "MixedReality.Toolkit.UXCore.Editor.Tests", "rootNamespace": "", "references": [ + "MixedReality.Toolkit.Core", "MixedReality.Toolkit.Core.TestUtilities", "MixedReality.Toolkit.UXCore", + "Unity.XR.Interaction.Toolkit", + "Unity.XR.CoreUtils", "UnityEngine.TestRunner", "UnityEditor.TestRunner" ], diff --git a/org.mixedrealitytoolkit.uxcore/Tests/Runtime/CanvasSliderVisualsRuntimeTests.cs b/org.mixedrealitytoolkit.uxcore/Tests/Runtime/CanvasSliderVisualsRuntimeTests.cs new file mode 100644 index 000000000..012f00d87 --- /dev/null +++ b/org.mixedrealitytoolkit.uxcore/Tests/Runtime/CanvasSliderVisualsRuntimeTests.cs @@ -0,0 +1,152 @@ +// Copyright (c) Mixed Reality Toolkit Contributors +// Licensed under the BSD 3-Clause + +// Disable "missing XML comment" warning for tests. While nice to have, this documentation is not required. +#pragma warning disable CS1591 + +using MixedReality.Toolkit.Input.Tests; +using NUnit.Framework; +using System.Collections; +using UnityEngine; +using UnityEngine.TestTools; +using UnityEngine.UI; + +namespace MixedReality.Toolkit.UX.Runtime.Tests +{ + /// + /// PlayMode runtime tests verifying CanvasSliderVisuals and Slider runtime behavior. + /// + public class CanvasSliderVisualsRuntimeTests : BaseRuntimeInputTests + { + private GameObject canvasObject; + private GameObject sliderObject; + private Slider slider; + private CanvasSliderVisuals visuals; + private RectTransform sliderStart; + private RectTransform sliderEnd; + private RectTransform trackArea; + private RectTransform handle; + private RectTransform fillVisual; + + public override IEnumerator Setup() + { + yield return base.Setup(); + + canvasObject = new GameObject("SliderParent", typeof(RectTransform), typeof(Canvas), typeof(CanvasScaler)); + canvasObject.transform.localScale = Vector3.one * 0.001f; + (canvasObject.transform as RectTransform).sizeDelta = Vector2.one * 200; + + sliderObject = new GameObject("Slider", typeof(RectTransform), typeof(Slider)); + sliderObject.SetActive(false); + sliderObject.transform.SetParent(canvasObject.transform, false); + sliderObject.transform.position = Vector3.zero; + (sliderObject.transform as RectTransform).sizeDelta = new Vector2(200f, 10f); + + slider = sliderObject.GetComponent(); + + GameObject startGo = new GameObject("SliderStart", typeof(RectTransform)); + startGo.transform.SetParent(sliderObject.transform, false); + sliderStart = startGo.GetComponent(); + + GameObject endGo = new GameObject("SliderEnd", typeof(RectTransform)); + endGo.transform.SetParent(sliderObject.transform, false); + sliderEnd = endGo.GetComponent(); + + GameObject trackGo = new GameObject("TrackArea", typeof(RectTransform)); + trackGo.transform.SetParent(sliderObject.transform, false); + trackArea = trackGo.GetComponent(); + trackArea.sizeDelta = new Vector2(200f, 10f); + + GameObject fillGo = new GameObject("FillVisual", typeof(RectTransform)); + fillGo.transform.SetParent(trackGo.transform, false); + fillVisual = fillGo.GetComponent(); + + GameObject handleGo = new GameObject("Handle", typeof(RectTransform)); + handleGo.transform.SetParent(trackGo.transform, false); + handle = handleGo.GetComponent(); + + BoxCollider trackCollider = trackGo.AddComponent(); + slider.TrackCollider = trackCollider; + typeof(Slider).GetField("handleTransform", System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic).SetValue(slider, handle.transform); + slider.SliderStart = sliderStart; + slider.SliderEnd = sliderEnd; + slider.MinValue = 0f; + slider.MaxValue = 1f; + slider.Value = 0f; + + visuals = sliderObject.AddComponent(); + visuals.TrackArea = trackArea; + visuals.Handle = handle; + visuals.FillVisual = fillVisual; + sliderObject.SetActive(true); + + yield return null; + } + + public override IEnumerator TearDown() + { + if (canvasObject != null) + { + Object.Destroy(canvasObject); + } + yield return base.TearDown(); + } + + [UnityTest] + public IEnumerator CanvasSliderVisuals_PlayMode_RespondsToSliderValueUpdate() + { + slider.Value = 0.75f; + yield return null; + + Assert.AreEqual(0.75f, handle.anchorMin.x, 0.001f, "Handle anchorMin.x should update to reflect 0.75 in PlayMode"); + Assert.AreEqual(0.75f, handle.anchorMax.x, 0.001f, "Handle anchorMax.x should update to reflect 0.75 in PlayMode"); + Assert.AreEqual(0.75f, fillVisual.anchorMax.x, 0.001f, "FillVisual anchorMax.x should update to reflect 0.75 in PlayMode"); + } + + [UnityTest] + public IEnumerator CanvasSliderVisuals_PlayMode_StepDivisionsSnapValueAndVisuals() + { + slider.MinValue = 0f; + slider.MaxValue = 10f; + slider.Value = 3.4f; + + slider.SliderStepDivisions = 10; + slider.UseSliderStepDivisions = true; + yield return null; + + Assert.AreEqual(3.0f, slider.Value, 0.0001f, "Slider value should snap to step division when enabled"); + Assert.AreEqual(0.3f, handle.anchorMin.x, 0.001f, "Handle should reflect snapped normalized value"); + } + + [UnityTest] + public IEnumerator CanvasSliderVisuals_PlayMode_SliderDirectionUpdatesLayout() + { + slider.Value = 0.5f; + + visuals.SliderDirection = CanvasSliderVisuals.Direction.RightToLeft; + yield return null; + Assert.AreEqual(new Vector2(1f, 0.5f), sliderStart.anchorMin); + Assert.AreEqual(new Vector2(0f, 0.5f), sliderEnd.anchorMin); + Assert.AreEqual(0.5f, handle.anchorMin.x, 0.001f); + + visuals.SliderDirection = CanvasSliderVisuals.Direction.BottomToTop; + yield return null; + Assert.AreEqual(new Vector2(0.5f, 0f), sliderStart.anchorMin); + Assert.AreEqual(new Vector2(0.5f, 1f), sliderEnd.anchorMin); + Assert.AreEqual(0.5f, handle.anchorMin.y, 0.001f); + + visuals.SliderDirection = CanvasSliderVisuals.Direction.TopToBottom; + yield return null; + Assert.AreEqual(new Vector2(0.5f, 1f), sliderStart.anchorMin); + Assert.AreEqual(new Vector2(0.5f, 0f), sliderEnd.anchorMin); + Assert.AreEqual(0.5f, handle.anchorMin.y, 0.001f); + + visuals.SliderDirection = CanvasSliderVisuals.Direction.LeftToRight; + yield return null; + Assert.AreEqual(new Vector2(0f, 0.5f), sliderStart.anchorMin); + Assert.AreEqual(new Vector2(1f, 0.5f), sliderEnd.anchorMin); + Assert.AreEqual(0.5f, handle.anchorMin.x, 0.001f); + } + } +} +#pragma warning restore CS1591 diff --git a/org.mixedrealitytoolkit.uxcore/Tests/Runtime/CanvasSliderVisualsRuntimeTests.cs.meta b/org.mixedrealitytoolkit.uxcore/Tests/Runtime/CanvasSliderVisualsRuntimeTests.cs.meta new file mode 100644 index 000000000..04cbb43c6 --- /dev/null +++ b/org.mixedrealitytoolkit.uxcore/Tests/Runtime/CanvasSliderVisualsRuntimeTests.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 2ba491c532904adb8e973b27786a82a7 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: