diff --git a/components/GradientSlider/OpenSolution.bat b/components/GradientSlider/OpenSolution.bat
new file mode 100644
index 000000000..814a56d4b
--- /dev/null
+++ b/components/GradientSlider/OpenSolution.bat
@@ -0,0 +1,3 @@
+@ECHO OFF
+
+powershell ..\..\tooling\ProjectHeads\GenerateSingleSampleHeads.ps1 -componentPath %CD% %*
\ No newline at end of file
diff --git a/components/GradientSlider/samples/Assets/icon.png b/components/GradientSlider/samples/Assets/icon.png
new file mode 100644
index 000000000..8435bcaa9
Binary files /dev/null and b/components/GradientSlider/samples/Assets/icon.png differ
diff --git a/components/GradientSlider/samples/Dependencies.props b/components/GradientSlider/samples/Dependencies.props
new file mode 100644
index 000000000..e622e1df4
--- /dev/null
+++ b/components/GradientSlider/samples/Dependencies.props
@@ -0,0 +1,31 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/components/GradientSlider/samples/GradientSlider.Samples.csproj b/components/GradientSlider/samples/GradientSlider.Samples.csproj
new file mode 100644
index 000000000..676ed9987
--- /dev/null
+++ b/components/GradientSlider/samples/GradientSlider.Samples.csproj
@@ -0,0 +1,15 @@
+
+
+
+
+ GradientSlider
+
+
+
+
+
+
+ GradientSliderSample.xaml
+
+
+
diff --git a/components/GradientSlider/samples/GradientSlider.md b/components/GradientSlider/samples/GradientSlider.md
new file mode 100644
index 000000000..197b06c43
--- /dev/null
+++ b/components/GradientSlider/samples/GradientSlider.md
@@ -0,0 +1,25 @@
+---
+title: GradientSlider
+author: githubaccount
+description: TODO: Your experiment's description here
+keywords: GradientSlider, Control, Layout
+dev_langs:
+ - csharp
+category: Controls
+subcategory: Layout
+discussion-id: 0
+issue-id: 0
+icon: assets/icon.png
+---
+
+
+
+
+
+
+
+
+
+# GradientSlider
+
+> [!SAMPLE GradientSliderSample]
diff --git a/components/GradientSlider/samples/GradientSliderSample.xaml b/components/GradientSlider/samples/GradientSliderSample.xaml
new file mode 100644
index 000000000..a89f18c6f
--- /dev/null
+++ b/components/GradientSlider/samples/GradientSliderSample.xaml
@@ -0,0 +1,23 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/components/GradientSlider/samples/GradientSliderSample.xaml.cs b/components/GradientSlider/samples/GradientSliderSample.xaml.cs
new file mode 100644
index 000000000..6ab91ce52
--- /dev/null
+++ b/components/GradientSlider/samples/GradientSliderSample.xaml.cs
@@ -0,0 +1,21 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using CommunityToolkit.WinUI.Controls;
+
+namespace GradientSliderExperiment.Samples;
+
+///
+/// An example sample page of a custom control inheriting from Panel.
+///
+
+[ToolkitSample(id: nameof(GradientSliderSample), "Custom control", description: $"A sample for showing how to create and use a {nameof(GradientSlider)} custom control.")]
+[ToolkitSampleBoolOption("CanAddStops", true, Title = nameof(GradientSlider.IsAddStopsEnabled))]
+public sealed partial class GradientSliderSample : Page
+{
+ public GradientSliderSample()
+ {
+ this.InitializeComponent();
+ }
+}
diff --git a/components/GradientSlider/src/CommunityToolkit.WinUI.Controls.GradientSlider.csproj b/components/GradientSlider/src/CommunityToolkit.WinUI.Controls.GradientSlider.csproj
new file mode 100644
index 000000000..80ce29607
--- /dev/null
+++ b/components/GradientSlider/src/CommunityToolkit.WinUI.Controls.GradientSlider.csproj
@@ -0,0 +1,14 @@
+
+
+
+
+ GradientSlider
+ This package contains GradientSlider.
+
+
+ CommunityToolkit.WinUI.Controls.GradientSliderRns
+
+
+
+
+
diff --git a/components/GradientSlider/src/Dependencies.props b/components/GradientSlider/src/Dependencies.props
new file mode 100644
index 000000000..e622e1df4
--- /dev/null
+++ b/components/GradientSlider/src/Dependencies.props
@@ -0,0 +1,31 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/components/GradientSlider/src/GradientSlider.Events.cs b/components/GradientSlider/src/GradientSlider.Events.cs
new file mode 100644
index 000000000..24939221f
--- /dev/null
+++ b/components/GradientSlider/src/GradientSlider.Events.cs
@@ -0,0 +1,49 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+namespace CommunityToolkit.WinUI.Controls;
+
+public partial class GradientSlider
+{
+ ///
+ /// Event raised when a thumb starts being dragged.
+ ///
+ public event DragStartedEventHandler? ThumbDragStarted;
+
+ ///
+ /// Event raised when a thumb ends being dragged.
+ ///
+ public event DragCompletedEventHandler? ThumbDragCompleted;
+
+ ///
+ /// Event raised when the gradient's value changes.
+ ///
+ public event EventHandler? ValueChanged;
+
+ ///
+ /// Called before the event occurs.
+ ///
+ /// Event data for the event.
+ protected virtual void OnThumbDragStarted(DragStartedEventArgs e)
+ {
+ ThumbDragStarted?.Invoke(this, e);
+ }
+
+ ///
+ /// Called before the event occurs.
+ ///
+ /// Event data for the event.
+ protected virtual void OnThumbDragCompleted(DragCompletedEventArgs e)
+ {
+ ThumbDragCompleted?.Invoke(this, e);
+ }
+
+ ///
+ /// Called before the event occurs.
+ ///
+ protected virtual void OnValueChanged()
+ {
+ ValueChanged?.Invoke(this, EventArgs.Empty);
+ }
+}
diff --git a/components/GradientSlider/src/GradientSlider.Input.cs b/components/GradientSlider/src/GradientSlider.Input.cs
new file mode 100644
index 000000000..a54444239
--- /dev/null
+++ b/components/GradientSlider/src/GradientSlider.Input.cs
@@ -0,0 +1,189 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+#if !WINAPPSDK
+using Windows.UI;
+#else
+using Microsoft.UI;
+using Microsoft.UI.Xaml.Controls.Primitives;
+#endif
+using Windows.System;
+
+namespace CommunityToolkit.WinUI.Controls;
+
+public partial class GradientSlider
+{
+ private void Thumb_DragStarted(object sender, DragStartedEventArgs e)
+ {
+ if (sender is not GradientSliderThumb thumb)
+ return;
+
+ _draggingThumb = thumb;
+ var xStart = Canvas.GetLeft(thumb);
+ var yStart = e.VerticalOffset;
+ _dragPosition = new Point(xStart, yStart);
+
+ OnThumbDragStarted(e);
+ }
+
+ private void Thumb_DragDelta(object sender, DragDeltaEventArgs e)
+ {
+ if (_containerCanvas is null)
+ return;
+
+ if (sender is not GradientSliderThumb thumb)
+ return;
+
+ _dragPosition.X += e.HorizontalChange;
+ _dragPosition.Y += e.VerticalChange;
+
+ HandleThumbDragging(thumb, _dragPosition);
+ }
+
+ private void Thumb_DragCompleted(object sender, DragCompletedEventArgs e)
+ {
+ _draggingThumb = null;
+
+ OnThumbDragCompleted(e);
+ OnValueChanged();
+ }
+
+ private void Thumb_KeyDown(object sender, KeyRoutedEventArgs e)
+ {
+ if (sender is not GradientSliderThumb thumb)
+ return;
+
+ var change = e.Key switch
+ {
+#if !HAS_UNO
+ VirtualKey.Left when FlowDirection is FlowDirection.RightToLeft => 0.05,
+ VirtualKey.Right when FlowDirection is FlowDirection.RightToLeft => -0.05,
+#endif
+
+ VirtualKey.Left => -0.01,
+ VirtualKey.Right => 0.01,
+
+ _ => 0,
+ };
+
+ if (change is not 0)
+ {
+ thumb.GradientStop.Offset = Math.Clamp(change + thumb.GradientStop.Offset, 0, 1);
+ UpdateThumbPosition(thumb);
+ e.Handled = true;
+ }
+ }
+
+ private void ContainerCanvas_PointerEntered(object sender, PointerRoutedEventArgs e)
+ {
+ if (_placeholderThumb is null)
+ return;
+
+ if (IsAddStopsEnabled)
+ {
+ _placeholderThumb.Visibility = Visibility.Visible;
+ }
+
+ VisualStateManager.GoToState(this, PointerOverState, false);
+ }
+
+ private void ContainerCanvas_PointerMoved(object sender, PointerRoutedEventArgs e)
+ {
+ if (_containerCanvas is null || _placeholderThumb is null)
+ return;
+
+ var position = e.GetCurrentPoint(_containerCanvas).Position;
+ var posX = position.X;
+
+ if (_draggingThumb is null)
+ {
+ // NOTE: This check could be made O(log(n)) by tracking the thumbs positions in a sorted list and running a binary search
+ _placeholderThumb.IsEnabled = !IsPointerOverThumb(posX);
+
+ var thumbPosition = posX - _placeholderThumb.ActualWidth / 2;
+ thumbPosition = Math.Clamp(thumbPosition, 0, _containerCanvas.ActualWidth - _placeholderThumb.ActualWidth);
+ Canvas.SetLeft(_placeholderThumb, thumbPosition);
+ }
+ else if (_draggingThumb.PointerCaptures?.Count is null or 0)
+ {
+ HandleThumbDragging(_draggingThumb, position);
+ }
+ }
+
+ private void ContainerCanvas_PointerExited(object sender, PointerRoutedEventArgs e)
+ {
+ if (_placeholderThumb is null)
+ return;
+
+ _placeholderThumb.Visibility = Visibility.Collapsed;
+ _placeholderThumb.IsEnabled = false;
+
+ VisualStateManager.GoToState(this, NormalState, false);
+ }
+
+ private void ContainerCanvas_PointerPressed(object sender, PointerRoutedEventArgs e)
+ {
+ if (_containerCanvas is null || _placeholderThumb is null)
+ return;
+
+ if (!IsAddStopsEnabled)
+ return;
+
+ var position = e.GetCurrentPoint(_containerCanvas).Position.X;
+ if (IsPointerOverThumb(position))
+ return;
+
+ _containerCanvas.CapturePointer(e.Pointer);
+
+ _placeholderThumb.IsEnabled = false;
+
+ var stop = new GradientStop()
+ {
+ Offset = position / _containerCanvas.ActualWidth,
+ Color = Colors.Black,
+ };
+
+ GradientStops.Add(stop);
+ _draggingThumb = AddStop(stop);
+ }
+
+ private void ContainerCanvas_PointerReleased(object sender, PointerRoutedEventArgs e)
+ {
+ if (_containerCanvas is null)
+ return;
+
+ _draggingThumb = null;
+ _containerCanvas.ReleasePointerCapture(e.Pointer);
+
+ OnValueChanged();
+ }
+
+ private bool IsPointerOverThumb(double position)
+ {
+ if (_containerCanvas is null)
+ return false;
+
+ foreach (var child in _containerCanvas.Children)
+ {
+ if (child is not GradientSliderThumb thumb)
+ continue;
+
+ var thumbPos = Canvas.GetLeft(thumb);
+ if (position > thumbPos - thumb.ActualWidth && position < thumbPos + (thumb.ActualWidth * 2))
+ return true;
+ }
+
+ return false;
+ }
+
+ private void HandleThumbDragging(GradientSliderThumb thumb, Point position)
+ {
+ if (_containerCanvas is null)
+ return;
+
+ var newPos = position.X / (_containerCanvas.ActualWidth - thumb.ActualWidth);
+ thumb.GradientStop.Offset = Math.Clamp(newPos, 0, 1);
+ UpdateThumbPosition(thumb);
+ }
+}
diff --git a/components/GradientSlider/src/GradientSlider.Properties.cs b/components/GradientSlider/src/GradientSlider.Properties.cs
new file mode 100644
index 000000000..1ec8f5c4e
--- /dev/null
+++ b/components/GradientSlider/src/GradientSlider.Properties.cs
@@ -0,0 +1,57 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+namespace CommunityToolkit.WinUI.Controls;
+
+public partial class GradientSlider
+{
+ ///
+ /// The backing for the property.
+ ///
+ public static readonly DependencyProperty GradientStopsProperty =
+ DependencyProperty.Register(nameof(GradientStops),
+ typeof(GradientStopCollection),
+ typeof(GradientSlider),
+ new PropertyMetadata(null, GradientStopsChangedCallback));
+
+ ///
+ /// The backing for the property.
+ ///
+ public static readonly DependencyProperty IsAddStopsEnabledProperty =
+ DependencyProperty.Register(nameof(IsAddStopsEnabled),
+ typeof(bool),
+ typeof(GradientSlider),
+ new PropertyMetadata(true));
+
+ ///
+ /// Gets or sets the being modified by the .
+ ///
+ public GradientStopCollection GradientStops
+ {
+ get => (GradientStopCollection)GetValue(GradientStopsProperty);
+ set => SetValue(GradientStopsProperty, value);
+ }
+
+ ///
+ /// Gets or sets whether or not the user can add new stops.
+ ///
+ public bool IsAddStopsEnabled
+ {
+ get => (bool)GetValue(IsAddStopsEnabledProperty);
+ set => SetValue(IsAddStopsEnabledProperty, value);
+ }
+
+ private static void GradientStopsChangedCallback(DependencyObject d, DependencyPropertyChangedEventArgs e)
+ {
+ if (d is not GradientSlider slider)
+ return;
+
+ if (slider._containerCanvas is null)
+ return;
+
+ // TODO: What happens if the gradient stop collection changes while the user is dragging a stop?
+
+ slider.RefreshThumbs();
+ }
+}
diff --git a/components/GradientSlider/src/GradientSlider.cs b/components/GradientSlider/src/GradientSlider.cs
new file mode 100644
index 000000000..3cb22a5fe
--- /dev/null
+++ b/components/GradientSlider/src/GradientSlider.cs
@@ -0,0 +1,203 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+#if !WINAPPSDK
+using Windows.UI;
+using Windows.UI.Xaml.Shapes;
+#else
+using Microsoft.UI;
+using Microsoft.UI.Xaml.Shapes;
+#endif
+
+namespace CommunityToolkit.WinUI.Controls;
+
+///
+/// A slider for gradient color input.
+///
+[TemplatePart(Name = "ContainerCanvas", Type = typeof(Canvas))]
+[TemplatePart(Name = "PlaceholderThumb", Type = typeof(Thumb))]
+[TemplatePart(Name = "BackgroundRectangle", Type = typeof(Rectangle))]
+public partial class GradientSlider : Control
+{
+ internal const string CommonStates = "CommonStates";
+ internal const string NormalState = "Normal";
+ internal const string PointerOverState = "PointerOver";
+ internal const string DisabledState = "Disabled";
+
+ private readonly Dictionary _stopThumbs = [];
+ private readonly Dictionary _stopCallbacks = [];
+
+ private Canvas? _containerCanvas;
+ private Thumb? _placeholderThumb;
+ private Rectangle? _backgroundRectangle;
+
+ private Point _dragPosition;
+ private GradientSliderThumb? _draggingThumb;
+
+ ///
+ /// Creates a new instance of the class.
+ ///
+ public GradientSlider()
+ {
+ this.DefaultStyleKey = typeof(GradientSlider);
+
+ GradientStops =
+ [
+ new GradientStop
+ {
+ Color = Colors.Black,
+ Offset = 0,
+ },
+ new GradientStop
+ {
+ Color = Colors.White,
+ Offset = 1,
+ }
+ ];
+ }
+
+ ///
+ protected override void OnApplyTemplate()
+ {
+ base.OnApplyTemplate();
+
+ if (_containerCanvas is not null)
+ {
+ _containerCanvas.SizeChanged -= ContainerCanvas_SizeChanged;
+ }
+
+ _containerCanvas = (Canvas)GetTemplateChild("ContainerCanvas");
+ _placeholderThumb = (Thumb)GetTemplateChild("PlaceholderThumb");
+ _backgroundRectangle = (Rectangle?)GetTemplateChild("BackgroundRectangle");
+
+ _containerCanvas.SizeChanged += ContainerCanvas_SizeChanged;
+
+ if (_placeholderThumb is not null)
+ {
+ _containerCanvas.PointerEntered += ContainerCanvas_PointerEntered;
+ _containerCanvas.PointerMoved += ContainerCanvas_PointerMoved;
+ _containerCanvas.PointerExited += ContainerCanvas_PointerExited;
+ _containerCanvas.PointerPressed += ContainerCanvas_PointerPressed;
+ _containerCanvas.PointerReleased += ContainerCanvas_PointerReleased;
+
+ _placeholderThumb.Visibility = Visibility.Collapsed;
+ }
+
+ RefreshThumbs();
+ }
+
+ private void ContainerCanvas_SizeChanged(object sender, SizeChangedEventArgs e)
+ => SyncThumbs();
+
+ private GradientSliderThumb? AddStop(GradientStop stop)
+ {
+ if (_containerCanvas is null)
+ {
+ throw new InvalidOperationException(nameof(_containerCanvas));
+ }
+
+ // Prepare a thumb for the gradient stop
+ var thumb = new GradientSliderThumb()
+ {
+ GradientStop = stop,
+ };
+
+ // Subcribe to events and callbacks
+ thumb.DragStarted += Thumb_DragStarted;
+ thumb.DragDelta += Thumb_DragDelta;
+ thumb.DragCompleted += Thumb_DragCompleted;
+ thumb.KeyDown += Thumb_KeyDown;
+ thumb.Loaded += Thumb_Loaded;
+ var callback = stop.RegisterPropertyChangedCallback(GradientStop.OffsetProperty, OnGradientStopOffsetChanged);
+ _stopCallbacks.Add(stop, callback);
+
+ // Track the thumb and add to the canvas
+ _stopThumbs.Add(stop, thumb);
+ _containerCanvas.Children.Add(thumb);
+
+ return thumb;
+ }
+
+ private void RemoveStop(GradientStop stop)
+ {
+ if (_containerCanvas is null)
+ return;
+
+ // Should this be an exception?
+ if (!_stopThumbs.TryGetValue(stop, out var thumb))
+ return;
+
+ // Unsubscribe from events and callbacks
+ thumb.DragStarted -= Thumb_DragStarted;
+ thumb.DragDelta -= Thumb_DragDelta;
+ thumb.DragCompleted -= Thumb_DragCompleted;
+ thumb.KeyDown -= Thumb_KeyDown;
+ thumb.Loaded -= Thumb_Loaded;
+ stop.UnregisterPropertyChangedCallback(GradientStop.OffsetProperty, _stopCallbacks[stop]);
+ _stopCallbacks.Remove(stop);
+
+ // Untrack the thumb and remove from the canvas
+ _stopThumbs.Remove(stop);
+ _containerCanvas.Children.Remove(thumb);
+ }
+
+ private void RefreshThumbs()
+ {
+ ClearThumbs();
+ foreach (var stop in GradientStops)
+ {
+ AddStop(stop);
+ }
+
+ SyncBackground();
+ }
+
+ private void ClearThumbs()
+ {
+ foreach (var (stop, _) in _stopThumbs)
+ RemoveStop(stop);
+ }
+
+ private void SyncThumbs()
+ {
+ foreach (var thumb in _stopThumbs.Values)
+ UpdateThumbPosition(thumb);
+ }
+
+ private void SyncBackground()
+ {
+ if (_containerCanvas is null || _backgroundRectangle is null)
+ return;
+
+ _backgroundRectangle.Fill = new LinearGradientBrush(GradientStops, 0);
+ }
+
+ private void Thumb_Loaded(object sender, RoutedEventArgs e)
+ {
+ if (sender is not GradientSliderThumb thumb)
+ return;
+
+ // Thumb position cannot be determined until it has loaded.
+ // Defer until the loading event
+ thumb.Loaded -= Thumb_Loaded;
+ UpdateThumbPosition(thumb);
+ }
+
+ private void OnGradientStopOffsetChanged(DependencyObject d, DependencyProperty e)
+ {
+ if (d is not GradientStop stop || !_stopThumbs.TryGetValue(stop, out var thumb))
+ return;
+
+ UpdateThumbPosition(thumb);
+ }
+
+ private void UpdateThumbPosition(GradientSliderThumb thumb)
+ {
+ if (_containerCanvas is null)
+ return;
+
+ var dragWidth = _containerCanvas.ActualWidth - thumb.ActualWidth;
+ Canvas.SetLeft(thumb, thumb.GradientStop.Offset * dragWidth);
+ }
+}
diff --git a/components/GradientSlider/src/GradientSliderStyle.xaml b/components/GradientSlider/src/GradientSliderStyle.xaml
new file mode 100644
index 000000000..276d74293
--- /dev/null
+++ b/components/GradientSlider/src/GradientSliderStyle.xaml
@@ -0,0 +1,107 @@
+
+
+
+
+
+ 0.5
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/components/GradientSlider/src/GradientSliderThumb.Events.cs b/components/GradientSlider/src/GradientSliderThumb.Events.cs
new file mode 100644
index 000000000..369de227e
--- /dev/null
+++ b/components/GradientSlider/src/GradientSliderThumb.Events.cs
@@ -0,0 +1,121 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+namespace CommunityToolkit.WinUI.Controls;
+
+public sealed partial class GradientSliderThumb : Control
+{
+ private bool _pointerOver;
+ private bool _pressed;
+ private bool _isDragging;
+ private Point _dragStartPosition;
+ private Point _lastPosition;
+
+ ///
+ /// Fires when the captures the pointer.
+ ///
+ public event DragStartedEventHandler? DragStarted;
+
+ ///
+ /// Fires as the is moved while capturing the pointer.
+ ///
+ public event DragDeltaEventHandler? DragDelta;
+
+ ///
+ /// Fires when the releases the captured pointer.
+ ///
+ public event DragCompletedEventHandler? DragCompleted;
+
+ private void GradientSliderThumb_PointerEntered(object sender, PointerRoutedEventArgs e)
+ {
+ _pointerOver = true;
+
+ if (!_pressed)
+ {
+ VisualStateManager.GoToState(this, PointerOverState, true);
+ }
+ }
+
+ private void GradientSliderThumb_PointerExited(object sender, PointerRoutedEventArgs e)
+ {
+ _pointerOver = false;
+
+ if (!_pressed)
+ {
+ VisualStateManager.GoToState(this, NormalState, true);
+ }
+ }
+
+ private void GradientSliderThumb_PointerPressed(object sender, PointerRoutedEventArgs e)
+ {
+ _pressed = true;
+ _isDragging = true;
+
+ CapturePointer(e.Pointer);
+
+ _dragStartPosition = e.GetCurrentPoint(null).Position;
+ _lastPosition = _dragStartPosition;
+
+ DragStarted?.Invoke(this,
+ new DragStartedEventArgs(_dragStartPosition.X, _dragStartPosition.Y));
+
+ VisualStateManager.GoToState(this, PressedState, true);
+ }
+
+ private void GradientSliderThumb_PointerMoved(object sender, PointerRoutedEventArgs e)
+ {
+ if (!_isDragging)
+ return;
+
+ var position = e.GetCurrentPoint(null).Position;
+
+ double deltaX = position.X - _lastPosition.X;
+ double deltaY = position.Y - _lastPosition.Y;
+
+ _lastPosition = position;
+
+ DragDelta?.Invoke(this, new DragDeltaEventArgs(deltaX, deltaY));
+ }
+
+ private void GradientSliderThumb_PointerReleased(object sender, PointerRoutedEventArgs e)
+ {
+ if (_isDragging)
+ {
+ var end = e.GetCurrentPoint(null).Position;
+
+ double totalX = end.X - _dragStartPosition.X;
+ double totalY = end.Y - _dragStartPosition.Y;
+
+ DragCompleted?.Invoke(this,
+ new DragCompletedEventArgs(totalX, totalY, false));
+ }
+
+ _isDragging = false;
+ _pressed = false;
+
+ ReleasePointerCapture(e.Pointer);
+
+ VisualStateManager.GoToState(this, _pointerOver ? PointerOverState : NormalState, true);
+ }
+
+ private void GradientSliderThumb_PointerCanceled(object sender, PointerRoutedEventArgs e)
+ {
+ if (_isDragging)
+ {
+ DragCompleted?.Invoke(this,
+ new DragCompletedEventArgs(0, 0, true));
+ }
+
+ _isDragging = false;
+ _pressed = false;
+
+ ReleasePointerCapture(e.Pointer);
+ VisualStateManager.GoToState(this, NormalState, true);
+ }
+
+ private void GradientSliderThumb_IsEnabledChanged(object sender, DependencyPropertyChangedEventArgs e)
+ {
+ VisualStateManager.GoToState(this, IsEnabled ? NormalState : DisabledState, true);
+ }
+}
diff --git a/components/GradientSlider/src/GradientSliderThumb.cs b/components/GradientSlider/src/GradientSliderThumb.cs
new file mode 100644
index 000000000..b8c637889
--- /dev/null
+++ b/components/GradientSlider/src/GradientSliderThumb.cs
@@ -0,0 +1,82 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using ColorPicker = Microsoft.UI.Xaml.Controls.ColorPicker;
+
+namespace CommunityToolkit.WinUI.Controls;
+
+///
+/// A thumb for adjusting a in a .
+///
+[TemplatePart(Name = "PART_ColorPicker", Type = typeof(ColorPicker))]
+[TemplatePart(Name = "PART_Border", Type = typeof(Border))]
+public sealed partial class GradientSliderThumb : Control
+{
+ internal const string CommonStates = "CommonStates";
+ internal const string NormalState = "Normal";
+ internal const string PointerOverState = "PointerOver";
+ internal const string PressedState = "Pressed";
+ internal const string DisabledState = "Disabled";
+
+ private Border? _border;
+ private ColorPicker? _colorPicker;
+
+ ///
+ /// The backing for the property.
+ ///
+ public static readonly DependencyProperty GradientStopProperty =
+ DependencyProperty.Register(nameof(GradientStop),
+ typeof(GradientStop),
+ typeof(GradientSliderThumb),
+ new PropertyMetadata(null));
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public GradientSliderThumb()
+ {
+ DefaultStyleKey = typeof(GradientSliderThumb);
+ }
+
+ ///
+ /// Gets or sets the the thumb controls.
+ ///
+ public GradientStop GradientStop
+ {
+ get => (GradientStop)GetValue(GradientStopProperty);
+ set => SetValue(GradientStopProperty, value);
+ }
+
+ ///
+ protected override void OnApplyTemplate()
+ {
+ base.OnApplyTemplate();
+
+ _border = (Border)GetTemplateChild("PART_Border");
+ _colorPicker = (ColorPicker)GetTemplateChild("PART_ColorPicker");
+
+ PointerEntered += GradientSliderThumb_PointerEntered;
+ PointerExited += GradientSliderThumb_PointerExited;
+ PointerPressed += GradientSliderThumb_PointerPressed;
+ PointerMoved += GradientSliderThumb_PointerMoved;
+ PointerReleased += GradientSliderThumb_PointerReleased;
+ PointerCanceled += GradientSliderThumb_PointerCanceled;
+ IsEnabledChanged += GradientSliderThumb_IsEnabledChanged;
+
+ _colorPicker.Color = GradientStop.Color;
+ _colorPicker.ColorChanged += ColorPicker_ColorChanged;
+
+ Tapped += GradientSliderThumb_Tapped;
+ }
+
+ private void ColorPicker_ColorChanged(ColorPicker sender, MUXC.ColorChangedEventArgs args)
+ {
+ GradientStop.Color = args.NewColor;
+ }
+
+ private void GradientSliderThumb_Tapped(object sender, TappedRoutedEventArgs e)
+ {
+ FlyoutBase.ShowAttachedFlyout(_border);
+ }
+}
diff --git a/components/GradientSlider/src/GradientSliderThumbStyle.xaml b/components/GradientSlider/src/GradientSliderThumbStyle.xaml
new file mode 100644
index 000000000..6541fdd27
--- /dev/null
+++ b/components/GradientSlider/src/GradientSliderThumbStyle.xaml
@@ -0,0 +1,126 @@
+
+
+
+
+
+
+
+
diff --git a/components/GradientSlider/src/MultiTarget.props b/components/GradientSlider/src/MultiTarget.props
new file mode 100644
index 000000000..b11c19426
--- /dev/null
+++ b/components/GradientSlider/src/MultiTarget.props
@@ -0,0 +1,9 @@
+
+
+
+ uwp;wasdk;wpf;wasm;linuxgtk;macos;ios;android;
+
+
\ No newline at end of file
diff --git a/components/GradientSlider/src/Themes/Generic.xaml b/components/GradientSlider/src/Themes/Generic.xaml
new file mode 100644
index 000000000..614177e26
--- /dev/null
+++ b/components/GradientSlider/src/Themes/Generic.xaml
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+
+
+
diff --git a/components/GradientSlider/tests/ExampleGradientSliderTestClass.cs b/components/GradientSlider/tests/ExampleGradientSliderTestClass.cs
new file mode 100644
index 000000000..5b6462bba
--- /dev/null
+++ b/components/GradientSlider/tests/ExampleGradientSliderTestClass.cs
@@ -0,0 +1,134 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using CommunityToolkit.Tooling.TestGen;
+using CommunityToolkit.Tests;
+using CommunityToolkit.WinUI.Controls;
+
+namespace GradientSliderTests;
+
+[TestClass]
+public partial class ExampleGradientSliderTestClass : VisualUITestBase
+{
+ // If you don't need access to UI objects directly or async code, use this pattern.
+ [TestMethod]
+ public void SimpleSynchronousExampleTest()
+ {
+ var assembly = typeof(GradientSlider).Assembly;
+ var type = assembly.GetType(typeof(GradientSlider).FullName ?? string.Empty);
+
+ Assert.IsNotNull(type, "Could not find GradientSlider type.");
+ Assert.AreEqual(typeof(GradientSlider), type, "Type of GradientSlider does not match expected type.");
+ }
+
+ // If you don't need access to UI objects directly, use this pattern.
+ [TestMethod]
+ public async Task SimpleAsyncExampleTest()
+ {
+ await Task.Delay(250);
+
+ Assert.IsTrue(true);
+ }
+
+ // Example that shows how to check for exception throwing.
+ [TestMethod]
+ public void SimpleExceptionCheckTest()
+ {
+ // If you need to check exceptions occur for invalid inputs, etc...
+ // Use Assert.ThrowsException to limit the scope to where you expect the error to occur.
+ // Otherwise, using the ExpectedException attribute could swallow or
+ // catch other issues in setup code.
+ Assert.ThrowsException(() => throw new NotImplementedException());
+ }
+
+ // The UIThreadTestMethod automatically dispatches to the UI for us to work with UI objects.
+ [UIThreadTestMethod]
+ public void SimpleUIAttributeExampleTest()
+ {
+ var component = new GradientSlider();
+ Assert.IsNotNull(component);
+ }
+
+ // The UIThreadTestMethod can also easily grab a XAML Page for us by passing its type as a parameter.
+ // This lets us actually test a control as it would behave within an actual application.
+ // The page will already be loaded by the time your test is called.
+ [UIThreadTestMethod]
+ public void SimpleUIExamplePageTest(ExampleGradientSliderTestPage page)
+ {
+ // You can use the Toolkit Visual Tree helpers here to find the component by type or name:
+ var component = page.FindDescendant();
+
+ Assert.IsNotNull(component);
+
+ var componentByName = page.FindDescendant("GradientSliderControl");
+
+ Assert.IsNotNull(componentByName);
+ }
+
+ // You can still do async work with a UIThreadTestMethod as well.
+ [UIThreadTestMethod]
+ public async Task SimpleAsyncUIExamplePageTest(ExampleGradientSliderTestPage page)
+ {
+ // This helper can be used to wait for a rendering pass to complete.
+ // Note, this is already done by loading a Page with the [UIThreadTestMethod] helper.
+ await CompositionTargetHelper.ExecuteAfterCompositionRenderingAsync(() => { });
+
+ var component = page.FindDescendant();
+
+ Assert.IsNotNull(component);
+ }
+
+ //// ----------------------------- ADVANCED TEST SCENARIOS -----------------------------
+
+ // If you need to use DataRow, you can use this pattern with the UI dispatch still.
+ // Otherwise, checkout the UIThreadTestMethod attribute above.
+ // See https://github.com/CommunityToolkit/Labs-Windows/issues/186
+ [TestMethod]
+ public async Task ComplexAsyncUIExampleTest()
+ {
+ await EnqueueAsync(() =>
+ {
+ var component = new GradientSlider();
+ Assert.IsNotNull(component);
+ });
+ }
+
+ // If you want to load other content not within a XAML page using the UIThreadTestMethod above.
+ // Then you can do that using the Load/UnloadTestContentAsync methods.
+ [TestMethod]
+ public async Task ComplexAsyncLoadUIExampleTest()
+ {
+ await EnqueueAsync(async () =>
+ {
+ var component = new GradientSlider();
+ Assert.IsNotNull(component);
+ Assert.IsFalse(component.IsLoaded);
+
+ await LoadTestContentAsync(component);
+
+ Assert.IsTrue(component.IsLoaded);
+
+ await UnloadTestContentAsync(component);
+
+ Assert.IsFalse(component.IsLoaded);
+ });
+ }
+
+ // You can still use the UIThreadTestMethod to remove the extra layer for the dispatcher as well:
+ [UIThreadTestMethod]
+ public async Task ComplexAsyncLoadUIExampleWithoutDispatcherTest()
+ {
+ var component = new GradientSlider();
+ Assert.IsNotNull(component);
+ Assert.IsFalse(component.IsLoaded);
+
+ await LoadTestContentAsync(component);
+
+ Assert.IsTrue(component.IsLoaded);
+
+ await UnloadTestContentAsync(component);
+
+ Assert.IsFalse(component.IsLoaded);
+ }
+}
diff --git a/components/GradientSlider/tests/ExampleGradientSliderTestPage.xaml b/components/GradientSlider/tests/ExampleGradientSliderTestPage.xaml
new file mode 100644
index 000000000..dbdc30833
--- /dev/null
+++ b/components/GradientSlider/tests/ExampleGradientSliderTestPage.xaml
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
diff --git a/components/GradientSlider/tests/ExampleGradientSliderTestPage.xaml.cs b/components/GradientSlider/tests/ExampleGradientSliderTestPage.xaml.cs
new file mode 100644
index 000000000..0fe89ed24
--- /dev/null
+++ b/components/GradientSlider/tests/ExampleGradientSliderTestPage.xaml.cs
@@ -0,0 +1,16 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+namespace GradientSliderTests;
+
+///
+/// An empty page that can be used on its own or navigated to within a Frame.
+///
+public sealed partial class ExampleGradientSliderTestPage : Page
+{
+ public ExampleGradientSliderTestPage()
+ {
+ this.InitializeComponent();
+ }
+}
diff --git a/components/GradientSlider/tests/GradientSlider.Tests.projitems b/components/GradientSlider/tests/GradientSlider.Tests.projitems
new file mode 100644
index 000000000..57a9b8fb5
--- /dev/null
+++ b/components/GradientSlider/tests/GradientSlider.Tests.projitems
@@ -0,0 +1,23 @@
+
+
+
+ $(MSBuildAllProjects);$(MSBuildThisFileFullPath)
+ true
+ 2F1B2F6D-D1F4-4895-96CE-F8CB3B3AF87F
+
+
+ GradientSliderTests
+
+
+
+
+ ExampleGradientSliderTestPage.xaml
+
+
+
+
+ Designer
+ MSBuild:Compile
+
+
+
\ No newline at end of file
diff --git a/components/GradientSlider/tests/GradientSlider.Tests.shproj b/components/GradientSlider/tests/GradientSlider.Tests.shproj
new file mode 100644
index 000000000..027ce84ad
--- /dev/null
+++ b/components/GradientSlider/tests/GradientSlider.Tests.shproj
@@ -0,0 +1,13 @@
+
+
+
+ 2F1B2F6D-D1F4-4895-96CE-F8CB3B3AF87F
+ 14.0
+
+
+
+
+
+
+
+