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: