diff --git a/components/RangeSelector/samples/RangeSelector.md b/components/RangeSelector/samples/RangeSelector.md
index 6b68bad4..33aff339 100644
--- a/components/RangeSelector/samples/RangeSelector.md
+++ b/components/RangeSelector/samples/RangeSelector.md
@@ -16,6 +16,22 @@ A `RangeSelector` is pretty similar to a regular `Slider`, and shares some of it
> [!Sample RangeSelectorSample]
+> [!NOTE]
+> Use 'VerticalAlignment="Stretch"' When 'Orientation="Vertical"'
+
+Like this:
+
+```xaml
+
+```
+
> [!NOTE]
> If you are using a RangeSelector within a ScrollViewer you'll need to add some codes. This is because by default, the ScrollViewer will block the thumbs of the RangeSelector to capture the pointer.
diff --git a/components/RangeSelector/samples/RangeSelectorSample.xaml b/components/RangeSelector/samples/RangeSelectorSample.xaml
index 2393fb3f..bd942013 100644
--- a/components/RangeSelector/samples/RangeSelectorSample.xaml
+++ b/components/RangeSelector/samples/RangeSelectorSample.xaml
@@ -12,11 +12,13 @@
MinHeight="86"
MaxWidth="560"
HorizontalAlignment="Stretch">
+
diff --git a/components/RangeSelector/samples/RangeSelectorSample.xaml.cs b/components/RangeSelector/samples/RangeSelectorSample.xaml.cs
index 823ca7fa..8511f99b 100644
--- a/components/RangeSelector/samples/RangeSelectorSample.xaml.cs
+++ b/components/RangeSelector/samples/RangeSelectorSample.xaml.cs
@@ -9,6 +9,7 @@ namespace RangeSelectorExperiment.Samples;
[ToolkitSampleNumericOption("Minimum", 0, 0, 100, 1, false, Title = "Minimum")]
[ToolkitSampleNumericOption("Maximum", 100, 0, 100, 1, false, Title = "Maximum")]
[ToolkitSampleNumericOption("StepFrequency", 1, 0, 10, 1, false, Title = "StepFrequency")]
+[ToolkitSampleMultiChoiceOption("OrientationMode", "Horizontal", "Vertical", Title = "Orientation")]
[ToolkitSampleBoolOption("Enable", true, Title = "IsEnabled")]
[ToolkitSample(id: nameof(RangeSelectorSample), "RangeSelector", description: $"A sample for showing how to create and use a {nameof(RangeSelector)} control.")]
diff --git a/components/RangeSelector/src/RangeSelector.Helpers.UVCoord.cs b/components/RangeSelector/src/RangeSelector.Helpers.UVCoord.cs
new file mode 100644
index 00000000..2f2a07e5
--- /dev/null
+++ b/components/RangeSelector/src/RangeSelector.Helpers.UVCoord.cs
@@ -0,0 +1,130 @@
+// 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;
+
+///
+/// A struct representing a coordinate in UV adjusted space.
+///
+[DebuggerDisplay("({U}u,{V}v)")]
+public struct UVCoord
+{
+ ///
+ /// Initializes a new instance of the struct.
+ ///
+ public UVCoord(Orientation orientation)
+ {
+ Orientation = orientation;
+ }
+
+ ///
+ /// Initializes a new instance of the struct.
+ ///
+ public UVCoord(double x, double y, Orientation orientation)
+ {
+ X = x;
+ Y = y;
+ Orientation = orientation;
+ }
+
+ ///
+ /// Initializes a new instance of the struct.
+ ///
+ public UVCoord(Point point, Orientation orientation) : this(point.X, point.Y, orientation)
+ {
+ }
+
+ ///
+ /// Initializes a new instance of the struct.
+ ///
+ public UVCoord(Size size, Orientation orientation) : this(size.Width, size.Height, orientation)
+ {
+ }
+
+ ///
+ /// Gets or sets the X coordinate.
+ ///
+ public double X { readonly get; set; }
+
+ ///
+ /// Gets or sets the Y coordinate.
+ ///
+ public double Y { readonly get; set; }
+
+ ///
+ /// Gets or sets the orientation for translation between the XY and UV coordinate systems.
+ ///
+ public Orientation Orientation { get; set; }
+
+ ///
+ /// Gets or sets the U coordinate.
+ ///
+ public double U
+ {
+ readonly get => Orientation is Orientation.Horizontal ? X : Y;
+ set
+ {
+ if (Orientation is Orientation.Horizontal)
+ {
+ X = value;
+ }
+ else
+ {
+ Y = value;
+ }
+ }
+ }
+
+ ///
+ /// Gets or sets the V coordinate.
+ ///
+ public double V
+ {
+ readonly get => Orientation is Orientation.Vertical ? X : Y;
+ set
+ {
+ if (Orientation is Orientation.Vertical)
+ {
+ X = value;
+ }
+ else
+ {
+ Y = value;
+ }
+ }
+ }
+
+ ///
+ /// Implicitly casts a to a .
+ ///
+ public static implicit operator Point(UVCoord uv) => new(uv.X, uv.Y);
+
+ ///
+ /// Implicitly casts a to a .
+ ///
+ public static implicit operator Size(UVCoord uv) => new(uv.X, uv.Y);
+
+ public static UVCoord operator +(UVCoord addend1, UVCoord addend2)
+ {
+ if (addend1.Orientation != addend2.Orientation)
+ {
+ throw new InvalidOperationException($"Cannot add {nameof(UVCoord)} with mismatched {nameof(Orientation)}.");
+ }
+
+ var xSum = addend1.X + addend2.X;
+ var ySum = addend1.Y + addend2.Y;
+ var orientation = addend1.Orientation;
+ return new UVCoord(xSum, ySum, orientation);
+ }
+
+ public static bool operator ==(UVCoord coord1, UVCoord coord2)
+ {
+ return coord1.U == coord2.U && coord1.V == coord2.V;
+ }
+
+ public static bool operator !=(UVCoord measure1, UVCoord measure2)
+ {
+ return !(measure1 == measure2);
+ }
+}
diff --git a/components/RangeSelector/src/RangeSelector.Input.Drag.cs b/components/RangeSelector/src/RangeSelector.Input.Drag.cs
index e8a8fd6a..5e5e051c 100644
--- a/components/RangeSelector/src/RangeSelector.Input.Drag.cs
+++ b/components/RangeSelector/src/RangeSelector.Input.Drag.cs
@@ -11,9 +11,13 @@ public partial class RangeSelector : Control
{
private void MinThumb_DragDelta(object sender, DragDeltaEventArgs e)
{
- _absolutePosition += e.HorizontalChange;
+ var uvChange = new UVCoord(e.HorizontalChange, e.VerticalChange, Orientation);
+ _absolutePosition += uvChange.U;
- RangeStart = DragThumb(_minThumb, 0, DragWidth(), _absolutePosition);
+ var maxThumbPos = GetCanvasPos(_maxThumb).U;
+ RangeStart = Orientation == Orientation.Horizontal
+ ? DragThumb(_minThumb, 0, maxThumbPos, _absolutePosition)
+ : DragThumb(_minThumb, maxThumbPos, DragWidth(), _absolutePosition);
if (_toolTipText != null)
{
@@ -23,9 +27,15 @@ private void MinThumb_DragDelta(object sender, DragDeltaEventArgs e)
private void MaxThumb_DragDelta(object sender, DragDeltaEventArgs e)
{
- _absolutePosition += e.HorizontalChange;
-
- RangeEnd = DragThumb(_maxThumb, 0, DragWidth(), _absolutePosition);
+ var uvChange = new UVCoord(e.HorizontalChange, e.VerticalChange, Orientation);
+ _absolutePosition += uvChange.U;
+// Adjust the position of the max thumb and update RangeEnd.
+// Note that max thumb has a lower U coordinate than min in vertical orientation,
+// so the valid range changes from between [0, minThumbPos] to [minThumbPos, DragWidth()]
+ var minThumbPos = GetCanvasPos(_minThumb).U;
+ RangeEnd = Orientation == Orientation.Horizontal
+ ? DragThumb(_maxThumb, minThumbPos, DragWidth(), _absolutePosition)
+ : DragThumb(_maxThumb, 0, minThumbPos, _absolutePosition);
if (_toolTipText != null)
{
@@ -67,7 +77,12 @@ private void Thumb_DragCompleted(object sender, DragCompletedEventArgs e)
private double DragWidth()
{
- return _containerCanvas!.ActualWidth - _maxThumb!.Width;
+ if (_containerCanvas == null || _maxThumb == null)
+ {
+ return 0;
+ }
+ return new UVCoord(_containerCanvas.ActualWidth, _containerCanvas.ActualHeight, Orientation).U
+ - new UVCoord(_maxThumb.Width, _maxThumb.Height, Orientation).U;
}
private double DragThumb(Thumb? thumb, double min, double max, double nextPos)
@@ -75,18 +90,38 @@ private double DragThumb(Thumb? thumb, double min, double max, double nextPos)
nextPos = Math.Max(min, nextPos);
nextPos = Math.Min(max, nextPos);
- Canvas.SetLeft(thumb, nextPos);
+ // Position the thumb
+ var thumbPos = new UVCoord(Orientation) { U = nextPos };
+ Canvas.SetLeft(thumb, thumbPos.X);
+ Canvas.SetTop(thumb, thumbPos.Y);
+ // Position the tooltip
if (_toolTip != null && thumb != null)
{
- var thumbCenter = nextPos + (thumb.Width / 2);
+ var thumbSize = new UVCoord(thumb.Width, thumb.Height, Orientation).U;
+ var thumbCenter = nextPos + (thumbSize / 2);
_toolTip.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity));
- var ttWidth = _toolTip.ActualWidth / 2;
+ var ttHalfSize = new UVCoord(_toolTip.DesiredSize, Orientation).U / 2;
+
+ var toolTipPos = new UVCoord(Orientation) { U = thumbCenter - ttHalfSize };
+ Canvas.SetLeft(_toolTip, toolTipPos.X);
+ Canvas.SetTop(_toolTip, toolTipPos.Y);
- Canvas.SetLeft(_toolTip, thumbCenter - ttWidth);
+ if (Orientation == Orientation.Vertical)
+ {
+ UpdateToolTipPositionForVertical();
+ }
}
- return Minimum + ((nextPos / DragWidth()) * (Maximum - Minimum));
+ // Calculate the range value
+ // Horizontal: left (0) = Minimum, right (DragWidth) = Maximum
+ // Vertical: top (0) = Maximum, bottom (DragWidth) = Minimum (inverted)
+ var ratio = nextPos / DragWidth();
+ var range = Maximum - Minimum;
+
+ return Orientation == Orientation.Horizontal
+ ? Minimum + (ratio * range)
+ : Maximum - (ratio * range);
}
private void Thumb_DragStarted(Thumb thumb)
@@ -94,21 +129,42 @@ private void Thumb_DragStarted(Thumb thumb)
var useMin = thumb == _minThumb;
var otherThumb = useMin ? _maxThumb : _minThumb;
- _absolutePosition = Canvas.GetLeft(thumb);
+ _absolutePosition = GetCanvasPos(thumb).U;
Canvas.SetZIndex(thumb, 10);
Canvas.SetZIndex(otherThumb, 0);
_oldValue = RangeStart;
if (_toolTip != null)
{
- _toolTip.Visibility = Visibility.Visible;
- var thumbCenter = _absolutePosition + (thumb.Width / 2);
- _toolTip.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity));
- var ttWidth = _toolTip.ActualWidth / 2;
- Canvas.SetLeft(_toolTip, thumbCenter - ttWidth);
-
- if (_toolTipText != null)
- UpdateToolTipText(this, _toolTipText, useMin ? RangeStart : RangeEnd);
+ if (Orientation == Orientation.Vertical && VerticalToolTipPlacement == VerticalToolTipPlacement.None)
+ {
+ _toolTip.Visibility = Visibility.Collapsed;
+ }
+ else
+ {
+ _toolTip.Visibility = Visibility.Visible;
+
+ // Update tooltip text first so Measure gets accurate size
+ if (_toolTipText != null)
+ {
+ UpdateToolTipText(this, _toolTipText, useMin ? RangeStart : RangeEnd);
+ }
+
+ _toolTip.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity));
+
+ var thumbSize = new UVCoord(thumb.Width, thumb.Height, Orientation).U;
+ var thumbCenter = _absolutePosition + (thumbSize / 2);
+ var ttHalfSize = new UVCoord(_toolTip.DesiredSize, Orientation).U / 2;
+
+ var toolTipPos = new UVCoord(Orientation) { U = thumbCenter - ttHalfSize };
+ Canvas.SetLeft(_toolTip, toolTipPos.X);
+ Canvas.SetTop(_toolTip, toolTipPos.Y);
+
+ if (Orientation == Orientation.Vertical)
+ {
+ UpdateToolTipPositionForVertical();
+ }
+ }
}
VisualStateManager.GoToState(this, useMin ? MinPressedState : MaxPressedState, true);
diff --git a/components/RangeSelector/src/RangeSelector.Input.Key.cs b/components/RangeSelector/src/RangeSelector.Input.Key.cs
index 24151c14..9e29c4ed 100644
--- a/components/RangeSelector/src/RangeSelector.Input.Key.cs
+++ b/components/RangeSelector/src/RangeSelector.Input.Key.cs
@@ -20,55 +20,68 @@ public partial class RangeSelector : Control
private void MinThumb_KeyDown(object sender, KeyRoutedEventArgs e)
{
- switch (e.Key)
+ var change = GetKeyboardChange(e.Key);
+
+ if (change != 0)
{
- case VirtualKey.Left:
- RangeStart -= StepFrequency;
- SyncThumbs(fromMinKeyDown: true);
- if (_toolTip != null)
- {
- _toolTip.Visibility = Visibility.Visible;
- }
+ RangeStart += change;
- e.Handled = true;
- break;
- case VirtualKey.Right:
- RangeStart += StepFrequency;
- SyncThumbs(fromMinKeyDown: true);
- if (_toolTip != null)
- {
- _toolTip.Visibility = Visibility.Visible;
- }
+ SyncThumbs(fromMinKeyDown: true);
+ ShowToolTip();
+ e.Handled = true;
+ }
+ }
- e.Handled = true;
- break;
+ private double GetKeyboardChange(VirtualKey key)
+ {
+ var isHorizontal = Orientation == Orientation.Horizontal;
+ var isRtl = FlowDirection == FlowDirection.RightToLeft;
+
+ if (isHorizontal)
+ {
+ switch (key)
+ {
+ case VirtualKey.Left:
+ return isRtl ? StepFrequency : -StepFrequency;
+ case VirtualKey.Right:
+ return isRtl ? -StepFrequency : StepFrequency;
+ }
+ }
+ else // Vertical
+ {
+ switch (key)
+ {
+ case VirtualKey.Down:
+ return -StepFrequency;
+ case VirtualKey.Up:
+ return StepFrequency;
+ }
}
+
+ return 0;
}
private void MaxThumb_KeyDown(object sender, KeyRoutedEventArgs e)
{
- switch (e.Key)
+ var change = GetKeyboardChange(e.Key);
+
+ if (change != 0)
{
- case VirtualKey.Left:
- RangeEnd -= StepFrequency;
- SyncThumbs(fromMaxKeyDown: true);
- if (_toolTip != null)
- {
- _toolTip.Visibility = Visibility.Visible;
- }
+ RangeEnd += change;
- e.Handled = true;
- break;
- case VirtualKey.Right:
- RangeEnd += StepFrequency;
- SyncThumbs(fromMaxKeyDown: true);
- if (_toolTip != null)
- {
- _toolTip.Visibility = Visibility.Visible;
- }
+ SyncThumbs(fromMaxKeyDown: true);
+ ShowToolTip();
+ e.Handled = true;
+ }
+ }
- e.Handled = true;
- break;
+ private void ShowToolTip()
+ {
+ var isHorizontal = Orientation == Orientation.Horizontal;
+ if (!isHorizontal && VerticalToolTipPlacement == VerticalToolTipPlacement.None) return;
+ if (_toolTip != null)
+ {
+ _toolTip.Visibility = Visibility.Visible;
}
}
@@ -78,6 +91,8 @@ private void Thumb_KeyUp(object sender, KeyRoutedEventArgs e)
{
case VirtualKey.Left:
case VirtualKey.Right:
+ case VirtualKey.Up:
+ case VirtualKey.Down:
if (_toolTip != null)
{
keyDebounceTimer.Debounce(
diff --git a/components/RangeSelector/src/RangeSelector.Input.Pointer.cs b/components/RangeSelector/src/RangeSelector.Input.Pointer.cs
index d27fd1be..1a216e40 100644
--- a/components/RangeSelector/src/RangeSelector.Input.Pointer.cs
+++ b/components/RangeSelector/src/RangeSelector.Input.Pointer.cs
@@ -16,8 +16,8 @@ private void ContainerCanvas_PointerEntered(object sender, PointerRoutedEventArg
private void ContainerCanvas_PointerExited(object sender, PointerRoutedEventArgs e)
{
- var position = e.GetCurrentPoint(_containerCanvas).Position.X;
- var normalizedPosition = ((position / DragWidth()) * (Maximum - Minimum)) + Minimum;
+ var point = new UVCoord(e.GetCurrentPoint(_containerCanvas).Position, Orientation);
+ var normalizedPosition = CalculateNormalizedPosition(point.U);
if (_pointerManipulatingMin)
{
@@ -40,13 +40,13 @@ private void ContainerCanvas_PointerExited(object sender, PointerRoutedEventArgs
_toolTip.Visibility = Visibility.Collapsed;
}
- VisualStateManager.GoToState(this, "Normal", false);
+ VisualStateManager.GoToState(this, NormalState, false);
}
private void ContainerCanvas_PointerReleased(object sender, PointerRoutedEventArgs e)
{
- var position = e.GetCurrentPoint(_containerCanvas).Position.X;
- var normalizedPosition = ((position / DragWidth()) * (Maximum - Minimum)) + Minimum;
+ var point = new UVCoord(e.GetCurrentPoint(_containerCanvas).Position, Orientation);
+ var normalizedPosition = CalculateNormalizedPosition(point.U);
if (_pointerManipulatingMin)
{
@@ -74,12 +74,15 @@ private void ContainerCanvas_PointerReleased(object sender, PointerRoutedEventAr
private void ContainerCanvas_PointerMoved(object sender, PointerRoutedEventArgs e)
{
- var position = e.GetCurrentPoint(_containerCanvas).Position.X;
- var normalizedPosition = ((position / DragWidth()) * (Maximum - Minimum)) + Minimum;
+ var point = new UVCoord(e.GetCurrentPoint(_containerCanvas).Position, Orientation);
if (_pointerManipulatingMin)
{
- RangeStart = DragThumb(_minThumb, 0, DragWidth(), position);
+ var maxThumbPos = GetCanvasPos(_maxThumb).U;
+ RangeStart = Orientation == Orientation.Horizontal
+ ? DragThumb(_minThumb, 0, maxThumbPos, point.U)
+ : DragThumb(_minThumb, maxThumbPos, DragWidth(), point.U);
+
if (_toolTipText is not null)
{
UpdateToolTipText(this, _toolTipText, RangeStart);
@@ -87,9 +90,13 @@ private void ContainerCanvas_PointerMoved(object sender, PointerRoutedEventArgs
}
else if (_pointerManipulatingMax)
{
+ var minThumbPos = GetCanvasPos(_minThumb).U;
+ RangeEnd = Orientation == Orientation.Horizontal
+ ? DragThumb(_maxThumb, minThumbPos, DragWidth(), point.U)
+ : DragThumb(_maxThumb, 0, minThumbPos, point.U);
+
if (_toolTipText is not null)
{
- RangeEnd = DragThumb(_maxThumb, 0, DragWidth(), position);
UpdateToolTipText(this, _toolTipText, RangeEnd);
}
}
@@ -97,8 +104,9 @@ private void ContainerCanvas_PointerMoved(object sender, PointerRoutedEventArgs
private void ContainerCanvas_PointerPressed(object sender, PointerRoutedEventArgs e)
{
- var position = e.GetCurrentPoint(_containerCanvas).Position.X;
- var normalizedPosition = position * Math.Abs(Maximum - Minimum) / DragWidth();
+ var point = new UVCoord(e.GetCurrentPoint(_containerCanvas).Position, Orientation);
+ var normalizedPosition = CalculateNormalizedPosition(point.U);
+
double upperValueDiff = Math.Abs(RangeEnd - normalizedPosition);
double lowerValueDiff = Math.Abs(RangeStart - normalizedPosition);
@@ -123,4 +131,18 @@ private void ContainerCanvas_PointerPressed(object sender, PointerRoutedEventArg
SyncThumbs();
}
+
+ ///
+ /// Converts a position along the primary axis to a normalized value in the range [Minimum, Maximum].
+ /// Handles vertical inversion automatically.
+ ///
+ private double CalculateNormalizedPosition(double position)
+ {
+ var ratio = position / DragWidth();
+ var range = Maximum - Minimum;
+
+ return Orientation == Orientation.Horizontal
+ ? (ratio * range) + Minimum
+ : Maximum - (ratio * range);
+ }
}
diff --git a/components/RangeSelector/src/RangeSelector.Properties.cs b/components/RangeSelector/src/RangeSelector.Properties.cs
index 6377df37..264a1ec0 100644
--- a/components/RangeSelector/src/RangeSelector.Properties.cs
+++ b/components/RangeSelector/src/RangeSelector.Properties.cs
@@ -9,6 +9,16 @@ namespace CommunityToolkit.WinUI.Controls;
///
public partial class RangeSelector : Control
{
+ ///
+ /// Identifies the property.
+ ///
+ public static readonly DependencyProperty OrientationProperty =
+ DependencyProperty.Register(
+ nameof(Orientation),
+ typeof(Orientation),
+ typeof(RangeSelector),
+ new PropertyMetadata(Orientation.Horizontal, OrientationChangedCallback));
+
///
/// Identifies the property.
///
@@ -59,6 +69,16 @@ public partial class RangeSelector : Control
typeof(RangeSelector),
new PropertyMetadata(DefaultStepFrequency));
+ ///
+ /// Identifies the property.
+ ///
+ public static readonly DependencyProperty VerticalToolTipPlacementProperty =
+ DependencyProperty.Register(
+ nameof(VerticalToolTipPlacement),
+ typeof(VerticalToolTipPlacement),
+ typeof(RangeSelector),
+ new PropertyMetadata(VerticalToolTipPlacement.Right));
+
///
/// Gets or sets the absolute minimum value of the range.
///
@@ -119,6 +139,67 @@ public double StepFrequency
set => SetValue(StepFrequencyProperty, value);
}
+ ///
+ /// Gets or sets the orientation of the range selector (horizontal or vertical).
+ ///
+ ///
+ /// The orientation of the range selector. Default is .
+ ///
+ public Orientation Orientation
+ {
+ get => (Orientation)GetValue(OrientationProperty);
+ set => SetValue(OrientationProperty, value);
+ }
+
+ ///
+ /// Gets or sets the placement of the tooltip for the vertical range selector.
+ /// This property only takes effect when is set to .
+ ///
+ ///
+ /// The placement of the tooltip. Default is .
+ ///
+ public VerticalToolTipPlacement VerticalToolTipPlacement
+ {
+ get => (VerticalToolTipPlacement)GetValue(VerticalToolTipPlacementProperty);
+ set => SetValue(VerticalToolTipPlacementProperty, value);
+ }
+
+ private static void OrientationChangedCallback(DependencyObject d, DependencyPropertyChangedEventArgs e)
+ {
+ var rangeSelector = d as RangeSelector;
+
+ if (rangeSelector == null || !rangeSelector._valuesAssigned)
+ {
+ return;
+ }
+
+ // Clear old Canvas position properties before switching orientation
+ if (rangeSelector._minThumb != null && rangeSelector._maxThumb != null && rangeSelector._activeRectangle != null)
+ {
+ var oldOrientation = (Orientation)e.OldValue;
+ var newOrientation = (Orientation)e.NewValue;
+
+ if (oldOrientation == Orientation.Horizontal && newOrientation == Orientation.Vertical)
+ {
+ // Clear horizontal properties
+ rangeSelector._minThumb.ClearValue(Canvas.LeftProperty);
+ rangeSelector._maxThumb.ClearValue(Canvas.LeftProperty);
+ rangeSelector._activeRectangle.ClearValue(Canvas.LeftProperty);
+ }
+ else if (oldOrientation == Orientation.Vertical && newOrientation == Orientation.Horizontal)
+ {
+ // Clear vertical properties
+ rangeSelector._minThumb.ClearValue(Canvas.TopProperty);
+ rangeSelector._maxThumb.ClearValue(Canvas.TopProperty);
+ rangeSelector._activeRectangle.ClearValue(Canvas.TopProperty);
+ }
+ }
+
+ VisualStateManager.GoToState(rangeSelector, rangeSelector.Orientation == Orientation.Horizontal ? HorizontalState : VerticalState, false);
+
+ rangeSelector.SyncThumbs();
+ }
+
private static void MinimumChangedCallback(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var rangeSelector = d as RangeSelector;
diff --git a/components/RangeSelector/src/RangeSelector.ToolTip.Placement.cs b/components/RangeSelector/src/RangeSelector.ToolTip.Placement.cs
new file mode 100644
index 00000000..3ff8ee28
--- /dev/null
+++ b/components/RangeSelector/src/RangeSelector.ToolTip.Placement.cs
@@ -0,0 +1,27 @@
+// 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;
+
+///
+/// Enumeration used to determine the placement of the tooltip
+/// for the vertical RangeSelector.
+///
+public enum VerticalToolTipPlacement
+{
+ ///
+ /// Tooltip is placed to the right of the thumb.
+ ///
+ Right,
+
+ ///
+ /// Tooltip is placed to the left of the thumb.
+ ///
+ Left,
+
+ ///
+ /// Tooltip is not displayed.
+ ///
+ None
+}
diff --git a/components/RangeSelector/src/RangeSelector.cs b/components/RangeSelector/src/RangeSelector.cs
index 7cf3bdbb..c539d6a2 100644
--- a/components/RangeSelector/src/RangeSelector.cs
+++ b/components/RangeSelector/src/RangeSelector.cs
@@ -16,6 +16,8 @@ namespace CommunityToolkit.WinUI.Controls;
[TemplateVisualState(Name = MinPressedState, GroupName = CommonStates)]
[TemplateVisualState(Name = MaxPressedState, GroupName = CommonStates)]
[TemplateVisualState(Name = DisabledState, GroupName = CommonStates)]
+[TemplateVisualState(Name = HorizontalState, GroupName = OrientationStates)]
+[TemplateVisualState(Name = VerticalState, GroupName = OrientationStates)]
[TemplatePart(Name = "OutOfRangeContentContainer", Type = typeof(Border))]
[TemplatePart(Name = "ActiveRectangle", Type = typeof(Rectangle))]
[TemplatePart(Name = "MinThumb", Type = typeof(Thumb))]
@@ -33,6 +35,9 @@ public partial class RangeSelector : Control
internal const string DisabledState = "Disabled";
internal const string MinPressedState = "MinPressed";
internal const string MaxPressedState = "MaxPressed";
+ internal const string OrientationStates = "OrientationStates";
+ internal const string HorizontalState = "Horizontal";
+ internal const string VerticalState = "Vertical";
private const double Epsilon = 0.01;
private const double DefaultMinimum = 0.0;
@@ -135,6 +140,7 @@ protected override void OnApplyTemplate()
}
VisualStateManager.GoToState(this, IsEnabled ? NormalState : DisabledState, false);
+ VisualStateManager.GoToState(this, Orientation == Orientation.Horizontal ? HorizontalState : VerticalState, false);
IsEnabledChanged += RangeSelector_IsEnabledChanged;
@@ -142,9 +148,19 @@ protected override void OnApplyTemplate()
var tb = new TextBlock { Text = Maximum.ToString() };
tb.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity));
+ // Ensure thumbs and active rectangle are synced after the control is fully loaded
+ Loaded -= RangeSelector_Loaded;
+ Loaded += RangeSelector_Loaded;
+
base.OnApplyTemplate();
}
+ private void RangeSelector_Loaded(object sender, RoutedEventArgs e)
+ {
+ Loaded -= RangeSelector_Loaded;
+ SyncThumbs();
+ }
+
private static void UpdateToolTipText(RangeSelector rangeSelector, TextBlock toolTip, double newValue)
{
if (toolTip != null)
@@ -237,24 +253,47 @@ private double MoveToStepFrequency(double rangeValue)
private void SyncThumbs(bool fromMinKeyDown = false, bool fromMaxKeyDown = false)
{
- if (_containerCanvas == null)
+ if (_containerCanvas == null || _minThumb == null || _maxThumb == null)
{
return;
}
- var relativeLeft = ((RangeStart - Minimum) / (Maximum - Minimum)) * DragWidth();
- var relativeRight = ((RangeEnd - Minimum) / (Maximum - Minimum)) * DragWidth();
+ var isHorizontal = Orientation == Orientation.Horizontal;
+ var relativeStart = ((RangeStart - Minimum) / (Maximum - Minimum)) * DragWidth();
+ var relativeEnd = ((RangeEnd - Minimum) / (Maximum - Minimum)) * DragWidth();
- Canvas.SetLeft(_minThumb, relativeLeft);
- Canvas.SetLeft(_maxThumb, relativeRight);
+ // Calculate canvas positions (vertical is inverted: min at bottom, max at top)
+ var minThumbCanvasPos = isHorizontal ? relativeStart : DragWidth() - relativeStart;
+ var maxThumbCanvasPos = isHorizontal ? relativeEnd : DragWidth() - relativeEnd;
+
+ // Position thumbs
+ var minPos = new UVCoord(Orientation) { U = minThumbCanvasPos };
+ var maxPos = new UVCoord(Orientation) { U = maxThumbCanvasPos };
+ Canvas.SetLeft(_minThumb, minPos.X);
+ Canvas.SetTop(_minThumb, minPos.Y);
+ Canvas.SetLeft(_maxThumb, maxPos.X);
+ Canvas.SetTop(_maxThumb, maxPos.Y);
if (fromMinKeyDown || fromMaxKeyDown)
{
- DragThumb(
- fromMinKeyDown ? _minThumb : _maxThumb,
- fromMinKeyDown ? 0 : Canvas.GetLeft(_minThumb),
- fromMinKeyDown ? Canvas.GetLeft(_maxThumb) : DragWidth(),
- fromMinKeyDown ? relativeLeft : relativeRight);
+ var thumb = fromMinKeyDown ? _minThumb : _maxThumb;
+ var canvasPos = fromMinKeyDown ? minThumbCanvasPos : maxThumbCanvasPos;
+
+ // Determine bounds for keyboard-driven drag
+ double min, max;
+ if (isHorizontal)
+ {
+ min = fromMinKeyDown ? 0 : GetCanvasPos(_minThumb).U;
+ max = fromMinKeyDown ? GetCanvasPos(_maxThumb).U : DragWidth();
+ }
+ else
+ {
+ min = fromMinKeyDown ? GetCanvasPos(_maxThumb).U : 0;
+ max = fromMinKeyDown ? DragWidth() : GetCanvasPos(_minThumb).U;
+ }
+
+ DragThumb(thumb, min, max, canvasPos);
+
if (_toolTipText != null)
{
UpdateToolTipText(this, _toolTipText, fromMinKeyDown ? RangeStart : RangeEnd);
@@ -266,29 +305,81 @@ private void SyncThumbs(bool fromMinKeyDown = false, bool fromMaxKeyDown = false
private void SyncActiveRectangle()
{
- if (_containerCanvas == null)
+ if (_containerCanvas == null || _minThumb == null || _maxThumb == null || _activeRectangle == null)
{
return;
}
- if (_minThumb == null)
+ var isHorizontal = Orientation == Orientation.Horizontal;
+ var minThumbPos = GetCanvasPos(_minThumb).U;
+ var maxThumbPos = GetCanvasPos(_maxThumb).U;
+
+ // For vertical, maxThumb is at top (lower canvas position), minThumb is at bottom
+ var startPos = isHorizontal ? minThumbPos : maxThumbPos;
+ var size = Math.Max(0, isHorizontal ? maxThumbPos - minThumbPos : minThumbPos - maxThumbPos);
+
+ // Position the active rectangle along the primary axis
+ var rectPos = new UVCoord(Orientation) { U = startPos };
+ Canvas.SetLeft(_activeRectangle, rectPos.X);
+ Canvas.SetTop(_activeRectangle, rectPos.Y);
+
+ // Center the active rectangle on the secondary axis
+ var containerSize = new UVCoord(_containerCanvas.ActualWidth, _containerCanvas.ActualHeight, Orientation);
+ var rectSize = new UVCoord(_activeRectangle.ActualWidth, _activeRectangle.ActualHeight, Orientation);
+ var secondaryPos = (containerSize.V - rectSize.V) / 2;
+
+ if (isHorizontal)
{
- return;
+ Canvas.SetTop(_activeRectangle, secondaryPos);
+ _activeRectangle.Width = size;
}
-
- if (_maxThumb == null)
+ else
{
- return;
+ Canvas.SetLeft(_activeRectangle, secondaryPos);
+ _activeRectangle.Height = size;
}
-
- var relativeLeft = Canvas.GetLeft(_minThumb);
- Canvas.SetLeft(_activeRectangle, relativeLeft);
- Canvas.SetTop(_activeRectangle, (_containerCanvas.ActualHeight - _activeRectangle!.ActualHeight) / 2);
- _activeRectangle.Width = Math.Max(0, Canvas.GetLeft(_maxThumb) - Canvas.GetLeft(_minThumb));
}
private void RangeSelector_IsEnabledChanged(object sender, DependencyPropertyChangedEventArgs e)
{
VisualStateManager.GoToState(this, IsEnabled ? NormalState : DisabledState, true);
}
+
+ private UVCoord GetCanvasPos(UIElement? element)
+ {
+ if (element == null)
+ {
+ return new UVCoord(Orientation);
+ }
+
+ var x = Canvas.GetLeft(element);
+ var y = Canvas.GetTop(element);
+ return new UVCoord(x, y, Orientation);
+ }
+
+ private void UpdateToolTipPositionForVertical()
+ {
+ if (_toolTip == null || _containerCanvas == null)
+ {
+ return;
+ }
+
+ // Offset to position tooltip beside the thumb
+ const double toolTipOffset = 52;
+
+ switch (VerticalToolTipPlacement)
+ {
+ case VerticalToolTipPlacement.Right:
+ Canvas.SetLeft(_toolTip, toolTipOffset);
+ break;
+ case VerticalToolTipPlacement.Left:
+ _toolTip.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity));
+ var toolTipWidth = _toolTip.DesiredSize.Width;
+ Canvas.SetLeft(_toolTip, -toolTipWidth - (toolTipOffset - _containerCanvas.ActualWidth));
+ break;
+ case VerticalToolTipPlacement.None:
+ _toolTip.Visibility = Visibility.Collapsed;
+ break;
+ }
+ }
}
diff --git a/components/RangeSelector/src/RangeSelector.xaml b/components/RangeSelector/src/RangeSelector.xaml
index 934c62a3..9f13e2ef 100644
--- a/components/RangeSelector/src/RangeSelector.xaml
+++ b/components/RangeSelector/src/RangeSelector.xaml
@@ -10,6 +10,10 @@
+ 150
+ 32
+
+