From 9cae9ebd590a8ef3188182dddb46ac823c05e044 Mon Sep 17 00:00:00 2001 From: Illustar0 Date: Wed, 3 Dec 2025 23:38:37 +0800 Subject: [PATCH 1/8] feat: add vertical orientation support for RangeSelector control --- .../RangeSelector/samples/RangeSelector.md | 6 ++ .../src/RangeSelector.Input.Drag.cs | 62 ++++++++++++--- .../src/RangeSelector.Input.Key.cs | 74 +++++++++-------- .../src/RangeSelector.Input.Pointer.cs | 42 +++++++--- .../src/RangeSelector.Properties.cs | 36 +++++++++ components/RangeSelector/src/RangeSelector.cs | 79 ++++++++++++++----- .../RangeSelector/src/RangeSelector.xaml | 22 +++++- 7 files changed, 247 insertions(+), 74 deletions(-) diff --git a/components/RangeSelector/samples/RangeSelector.md b/components/RangeSelector/samples/RangeSelector.md index 6b68bad4..530c0c0d 100644 --- a/components/RangeSelector/samples/RangeSelector.md +++ b/components/RangeSelector/samples/RangeSelector.md @@ -16,6 +16,12 @@ A `RangeSelector` is pretty similar to a regular `Slider`, and shares some of it > [!Sample RangeSelectorSample] +## Vertical Orientation + +The `RangeSelector` also supports vertical orientation. Set the `Orientation` property to `Vertical` to display the range selector vertically. + +> [!Sample RangeSelectorVerticalSample] + > [!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/src/RangeSelector.Input.Drag.cs b/components/RangeSelector/src/RangeSelector.Input.Drag.cs index e8a8fd6a..8a1225f5 100644 --- a/components/RangeSelector/src/RangeSelector.Input.Drag.cs +++ b/components/RangeSelector/src/RangeSelector.Input.Drag.cs @@ -11,9 +11,12 @@ public partial class RangeSelector : Control { private void MinThumb_DragDelta(object sender, DragDeltaEventArgs e) { - _absolutePosition += e.HorizontalChange; + var isHorizontal = Orientation == Orientation.Horizontal; + _absolutePosition += isHorizontal ? e.HorizontalChange : e.VerticalChange; - RangeStart = DragThumb(_minThumb, 0, DragWidth(), _absolutePosition); + RangeStart = isHorizontal + ? DragThumb(_minThumb, 0, Canvas.GetLeft(_maxThumb), _absolutePosition) + : DragThumbVertical(_minThumb, Canvas.GetTop(_maxThumb), DragWidth(), _absolutePosition); if (_toolTipText != null) { @@ -23,9 +26,12 @@ private void MinThumb_DragDelta(object sender, DragDeltaEventArgs e) private void MaxThumb_DragDelta(object sender, DragDeltaEventArgs e) { - _absolutePosition += e.HorizontalChange; + var isHorizontal = Orientation == Orientation.Horizontal; + _absolutePosition += isHorizontal ? e.HorizontalChange : e.VerticalChange; - RangeEnd = DragThumb(_maxThumb, 0, DragWidth(), _absolutePosition); + RangeEnd = isHorizontal + ? DragThumb(_maxThumb, Canvas.GetLeft(_minThumb), DragWidth(), _absolutePosition) + : DragThumbVertical(_maxThumb, 0, Canvas.GetTop(_minThumb), _absolutePosition); if (_toolTipText != null) { @@ -67,7 +73,14 @@ private void Thumb_DragCompleted(object sender, DragCompletedEventArgs e) private double DragWidth() { - return _containerCanvas!.ActualWidth - _maxThumb!.Width; + if (Orientation == Orientation.Horizontal) + { + return _containerCanvas!.ActualWidth - _maxThumb!.Width; + } + else + { + return _containerCanvas!.ActualHeight - _maxThumb!.Height; + } } private double DragThumb(Thumb? thumb, double min, double max, double nextPos) @@ -89,12 +102,33 @@ private double DragThumb(Thumb? thumb, double min, double max, double nextPos) return Minimum + ((nextPos / DragWidth()) * (Maximum - Minimum)); } + private double DragThumbVertical(Thumb? thumb, double min, double max, double nextPos) + { + nextPos = Math.Max(min, nextPos); + nextPos = Math.Min(max, nextPos); + + Canvas.SetTop(thumb, nextPos); + + if (_toolTip != null && thumb != null) + { + var thumbCenter = nextPos + (thumb.Height / 2); + _toolTip.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity)); + var ttHeight = _toolTip.ActualHeight / 2; + + Canvas.SetTop(_toolTip, thumbCenter - ttHeight); + } + + // Invert: top position (0) = Maximum, bottom position (DragWidth) = Minimum + return Maximum - ((nextPos / DragWidth()) * (Maximum - Minimum)); + } + private void Thumb_DragStarted(Thumb thumb) { var useMin = thumb == _minThumb; var otherThumb = useMin ? _maxThumb : _minThumb; + var isHorizontal = Orientation == Orientation.Horizontal; - _absolutePosition = Canvas.GetLeft(thumb); + _absolutePosition = isHorizontal ? Canvas.GetLeft(thumb) : Canvas.GetTop(thumb); Canvas.SetZIndex(thumb, 10); Canvas.SetZIndex(otherThumb, 0); _oldValue = RangeStart; @@ -102,13 +136,23 @@ private void Thumb_DragStarted(Thumb thumb) 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 (isHorizontal) + { + var thumbCenter = _absolutePosition + (thumb.Width / 2); + Canvas.SetLeft(_toolTip, thumbCenter - (_toolTip.ActualWidth / 2)); + } + else + { + var thumbCenter = _absolutePosition + (thumb.Height / 2); + Canvas.SetTop(_toolTip, thumbCenter - (_toolTip.ActualHeight / 2)); + } if (_toolTipText != null) + { UpdateToolTipText(this, _toolTipText, useMin ? RangeStart : RangeEnd); + } } 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..75d1b957 100644 --- a/components/RangeSelector/src/RangeSelector.Input.Key.cs +++ b/components/RangeSelector/src/RangeSelector.Input.Key.cs @@ -20,56 +20,64 @@ public partial class RangeSelector : Control private void MinThumb_KeyDown(object sender, KeyRoutedEventArgs e) { + var isHorizontal = Orientation == Orientation.Horizontal; + var handled = false; + switch (e.Key) { - case VirtualKey.Left: + case VirtualKey.Left when isHorizontal: + case VirtualKey.Down when !isHorizontal: RangeStart -= StepFrequency; - SyncThumbs(fromMinKeyDown: true); - if (_toolTip != null) - { - _toolTip.Visibility = Visibility.Visible; - } - - e.Handled = true; + handled = true; break; - case VirtualKey.Right: + case VirtualKey.Right when isHorizontal: + case VirtualKey.Up when !isHorizontal: RangeStart += StepFrequency; - SyncThumbs(fromMinKeyDown: true); - if (_toolTip != null) - { - _toolTip.Visibility = Visibility.Visible; - } - - e.Handled = true; + handled = true; break; } + + if (handled) + { + SyncThumbs(fromMinKeyDown: true); + ShowToolTip(); + e.Handled = true; + } } private void MaxThumb_KeyDown(object sender, KeyRoutedEventArgs e) { + var isHorizontal = Orientation == Orientation.Horizontal; + var handled = false; + switch (e.Key) { - case VirtualKey.Left: + case VirtualKey.Left when isHorizontal: + case VirtualKey.Down when !isHorizontal: RangeEnd -= StepFrequency; - SyncThumbs(fromMaxKeyDown: true); - if (_toolTip != null) - { - _toolTip.Visibility = Visibility.Visible; - } - - e.Handled = true; + handled = true; break; - case VirtualKey.Right: + case VirtualKey.Right when isHorizontal: + case VirtualKey.Up when !isHorizontal: RangeEnd += StepFrequency; - SyncThumbs(fromMaxKeyDown: true); - if (_toolTip != null) - { - _toolTip.Visibility = Visibility.Visible; - } - - e.Handled = true; + handled = true; break; } + + if (handled) + { + SyncThumbs(fromMaxKeyDown: true); + ShowToolTip(); + e.Handled = true; + } + } + + private void ShowToolTip() + { + if (_toolTip != null) + { + _toolTip.Visibility = Visibility.Visible; + } } private void Thumb_KeyUp(object sender, KeyRoutedEventArgs e) @@ -78,6 +86,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..10802b4f 100644 --- a/components/RangeSelector/src/RangeSelector.Input.Pointer.cs +++ b/components/RangeSelector/src/RangeSelector.Input.Pointer.cs @@ -16,8 +16,12 @@ 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 isHorizontal = Orientation == Orientation.Horizontal; + var point = e.GetCurrentPoint(_containerCanvas).Position; + var position = isHorizontal ? point.X : point.Y; + var normalizedPosition = isHorizontal + ? ((position / DragWidth()) * (Maximum - Minimum)) + Minimum + : Maximum - ((position / DragWidth()) * (Maximum - Minimum)); if (_pointerManipulatingMin) { @@ -40,13 +44,17 @@ 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 isHorizontal = Orientation == Orientation.Horizontal; + var point = e.GetCurrentPoint(_containerCanvas).Position; + var position = isHorizontal ? point.X : point.Y; + var normalizedPosition = isHorizontal + ? ((position / DragWidth()) * (Maximum - Minimum)) + Minimum + : Maximum - ((position / DragWidth()) * (Maximum - Minimum)); if (_pointerManipulatingMin) { @@ -74,12 +82,16 @@ 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 isHorizontal = Orientation == Orientation.Horizontal; + var point = e.GetCurrentPoint(_containerCanvas).Position; + var position = isHorizontal ? point.X : point.Y; if (_pointerManipulatingMin) { - RangeStart = DragThumb(_minThumb, 0, DragWidth(), position); + RangeStart = isHorizontal + ? DragThumb(_minThumb, 0, Canvas.GetLeft(_maxThumb), position) + : DragThumbVertical(_minThumb, Canvas.GetTop(_maxThumb), DragWidth(), position); + if (_toolTipText is not null) { UpdateToolTipText(this, _toolTipText, RangeStart); @@ -87,9 +99,12 @@ private void ContainerCanvas_PointerMoved(object sender, PointerRoutedEventArgs } else if (_pointerManipulatingMax) { + RangeEnd = isHorizontal + ? DragThumb(_maxThumb, Canvas.GetLeft(_minThumb), DragWidth(), position) + : DragThumbVertical(_maxThumb, 0, Canvas.GetTop(_minThumb), position); + if (_toolTipText is not null) { - RangeEnd = DragThumb(_maxThumb, 0, DragWidth(), position); UpdateToolTipText(this, _toolTipText, RangeEnd); } } @@ -97,8 +112,13 @@ 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 isHorizontal = Orientation == Orientation.Horizontal; + var point = e.GetCurrentPoint(_containerCanvas).Position; + var position = isHorizontal ? point.X : point.Y; + var normalizedPosition = isHorizontal + ? position * Math.Abs(Maximum - Minimum) / DragWidth() + : (Maximum - Minimum) - (position * Math.Abs(Maximum - Minimum) / DragWidth()); + double upperValueDiff = Math.Abs(RangeEnd - normalizedPosition); double lowerValueDiff = Math.Abs(RangeStart - normalizedPosition); diff --git a/components/RangeSelector/src/RangeSelector.Properties.cs b/components/RangeSelector/src/RangeSelector.Properties.cs index 6377df37..a58dd193 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. /// @@ -119,6 +129,32 @@ 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); + } + + private static void OrientationChangedCallback(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + var rangeSelector = d as RangeSelector; + + if (rangeSelector == null || !rangeSelector._valuesAssigned) + { + return; + } + + VisualStateManager.GoToState(rangeSelector, rangeSelector.Orientation == Orientation.Horizontal ? HorizontalState : VerticalState, true); + + rangeSelector.SyncThumbs(); + } + private static void MinimumChangedCallback(DependencyObject d, DependencyPropertyChangedEventArgs e) { var rangeSelector = d as RangeSelector; diff --git a/components/RangeSelector/src/RangeSelector.cs b/components/RangeSelector/src/RangeSelector.cs index 7cf3bdbb..cddc271f 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,18 @@ 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; + 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 +252,46 @@ 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 relativeStart = ((RangeStart - Minimum) / (Maximum - Minimum)) * DragWidth(); + var relativeEnd = ((RangeEnd - Minimum) / (Maximum - Minimum)) * DragWidth(); + var isHorizontal = Orientation == Orientation.Horizontal; - Canvas.SetLeft(_minThumb, relativeLeft); - Canvas.SetLeft(_maxThumb, relativeRight); + if (isHorizontal) + { + Canvas.SetLeft(_minThumb, relativeStart); + Canvas.SetLeft(_maxThumb, relativeEnd); + } + else + { + // Vertical: invert positions so min is at bottom, max is at top + Canvas.SetTop(_minThumb, DragWidth() - relativeStart); + Canvas.SetTop(_maxThumb, DragWidth() - relativeEnd); + } 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 position = fromMinKeyDown ? relativeStart : relativeEnd; + + if (isHorizontal) + { + var min = fromMinKeyDown ? 0 : Canvas.GetLeft(_minThumb); + var max = fromMinKeyDown ? Canvas.GetLeft(_maxThumb) : DragWidth(); + DragThumb(thumb, min, max, position); + } + else + { + var invertedPosition = DragWidth() - position; + var min = fromMinKeyDown ? Canvas.GetTop(_maxThumb) : 0; + var max = fromMinKeyDown ? DragWidth() : Canvas.GetTop(_minThumb); + DragThumbVertical(thumb, min, max, invertedPosition); + } + if (_toolTipText != null) { UpdateToolTipText(this, _toolTipText, fromMinKeyDown ? RangeStart : RangeEnd); @@ -266,25 +303,25 @@ 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) + if (Orientation == Orientation.Horizontal) { - return; + 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) - relativeLeft); } - - if (_maxThumb == null) + else { - return; + var relativeTop = Canvas.GetTop(_maxThumb); + Canvas.SetTop(_activeRectangle, relativeTop); + Canvas.SetLeft(_activeRectangle, (_containerCanvas.ActualWidth - _activeRectangle.ActualWidth) / 2); + _activeRectangle.Height = Math.Max(0, Canvas.GetTop(_minThumb) - relativeTop); } - - 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) diff --git a/components/RangeSelector/src/RangeSelector.xaml b/components/RangeSelector/src/RangeSelector.xaml index 934c62a3..dfd2af3b 100644 --- a/components/RangeSelector/src/RangeSelector.xaml +++ b/components/RangeSelector/src/RangeSelector.xaml @@ -17,7 +17,8 @@ - + + @@ -246,6 +247,25 @@ + + + + + + + + + + + + + + + + + + + Date: Thu, 4 Dec 2025 00:22:57 +0800 Subject: [PATCH 2/8] fix: add missing sample --- .../samples/RangeSelectorVerticalSample.xaml | 25 +++++++++++++++++++ .../RangeSelectorVerticalSample.xaml.cs | 21 ++++++++++++++++ 2 files changed, 46 insertions(+) create mode 100644 components/RangeSelector/samples/RangeSelectorVerticalSample.xaml create mode 100644 components/RangeSelector/samples/RangeSelectorVerticalSample.xaml.cs diff --git a/components/RangeSelector/samples/RangeSelectorVerticalSample.xaml b/components/RangeSelector/samples/RangeSelectorVerticalSample.xaml new file mode 100644 index 00000000..410e2524 --- /dev/null +++ b/components/RangeSelector/samples/RangeSelectorVerticalSample.xaml @@ -0,0 +1,25 @@ + + + + + + + diff --git a/components/RangeSelector/samples/RangeSelectorVerticalSample.xaml.cs b/components/RangeSelector/samples/RangeSelectorVerticalSample.xaml.cs new file mode 100644 index 00000000..69fc7b7d --- /dev/null +++ b/components/RangeSelector/samples/RangeSelectorVerticalSample.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 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")] +[ToolkitSampleBoolOption("Enable", true, Title = "IsEnabled")] + +[ToolkitSample(id: nameof(RangeSelectorVerticalSample), "Vertical RangeSelector", description: $"A sample for showing how to create and use a vertical {nameof(RangeSelector)} control.")] +public sealed partial class RangeSelectorVerticalSample : Page +{ + public RangeSelectorVerticalSample() + { + this.InitializeComponent(); + } +} From bb2ae926a5f3cc687a87a612e9ce26d7eb08bbfc Mon Sep 17 00:00:00 2001 From: Illustar0 Date: Thu, 4 Dec 2025 00:28:04 +0800 Subject: [PATCH 3/8] fix: correct vertical normalized position calculation logic Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- components/RangeSelector/src/RangeSelector.Input.Pointer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/RangeSelector/src/RangeSelector.Input.Pointer.cs b/components/RangeSelector/src/RangeSelector.Input.Pointer.cs index 10802b4f..fbd2023f 100644 --- a/components/RangeSelector/src/RangeSelector.Input.Pointer.cs +++ b/components/RangeSelector/src/RangeSelector.Input.Pointer.cs @@ -117,7 +117,7 @@ private void ContainerCanvas_PointerPressed(object sender, PointerRoutedEventArg var position = isHorizontal ? point.X : point.Y; var normalizedPosition = isHorizontal ? position * Math.Abs(Maximum - Minimum) / DragWidth() - : (Maximum - Minimum) - (position * Math.Abs(Maximum - Minimum) / DragWidth()); + : Maximum - ((position / DragWidth()) * (Maximum - Minimum)); double upperValueDiff = Math.Abs(RangeEnd - normalizedPosition); double lowerValueDiff = Math.Abs(RangeStart - normalizedPosition); From 2707b0f63f595f1afb963e2e74ac76758d01778b Mon Sep 17 00:00:00 2001 From: Illustar0 Date: Thu, 4 Dec 2025 00:31:40 +0800 Subject: [PATCH 4/8] refactor: simplify orientation check using ternary expression Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../RangeSelector/src/RangeSelector.Input.Drag.cs | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/components/RangeSelector/src/RangeSelector.Input.Drag.cs b/components/RangeSelector/src/RangeSelector.Input.Drag.cs index 8a1225f5..3f375e8d 100644 --- a/components/RangeSelector/src/RangeSelector.Input.Drag.cs +++ b/components/RangeSelector/src/RangeSelector.Input.Drag.cs @@ -73,14 +73,9 @@ private void Thumb_DragCompleted(object sender, DragCompletedEventArgs e) private double DragWidth() { - if (Orientation == Orientation.Horizontal) - { - return _containerCanvas!.ActualWidth - _maxThumb!.Width; - } - else - { - return _containerCanvas!.ActualHeight - _maxThumb!.Height; - } + return Orientation == Orientation.Horizontal + ? _containerCanvas!.ActualWidth - _maxThumb!.Width + : _containerCanvas!.ActualHeight - _maxThumb!.Height; } private double DragThumb(Thumb? thumb, double min, double max, double nextPos) From 66386894388603f7dabdf40bd4e2abe547396ed9 Mon Sep 17 00:00:00 2001 From: Illustar0 Date: Thu, 4 Dec 2025 11:26:06 +0800 Subject: [PATCH 5/8] fix: prevent multiple subscriptions to Loaded event Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- components/RangeSelector/src/RangeSelector.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/components/RangeSelector/src/RangeSelector.cs b/components/RangeSelector/src/RangeSelector.cs index cddc271f..082f093f 100644 --- a/components/RangeSelector/src/RangeSelector.cs +++ b/components/RangeSelector/src/RangeSelector.cs @@ -149,6 +149,7 @@ protected override void OnApplyTemplate() 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(); From bbdcb14ffc7528b954c1baebd76ea4be314f4418 Mon Sep 17 00:00:00 2001 From: Illustar0 Date: Fri, 5 Dec 2025 01:17:11 +0800 Subject: [PATCH 6/8] feat: implement vertical tooltip placement for RangeSelector control --- .../src/RangeSelector.Input.Drag.cs | 37 ++++++++++++------- .../src/RangeSelector.Properties.cs | 23 ++++++++++++ .../src/RangeSelector.ToolTip.Placement.cs | 27 ++++++++++++++ components/RangeSelector/src/RangeSelector.cs | 26 +++++++++++++ .../RangeSelector/src/RangeSelector.xaml | 2 +- 5 files changed, 101 insertions(+), 14 deletions(-) create mode 100644 components/RangeSelector/src/RangeSelector.ToolTip.Placement.cs diff --git a/components/RangeSelector/src/RangeSelector.Input.Drag.cs b/components/RangeSelector/src/RangeSelector.Input.Drag.cs index 3f375e8d..0d4a65ed 100644 --- a/components/RangeSelector/src/RangeSelector.Input.Drag.cs +++ b/components/RangeSelector/src/RangeSelector.Input.Drag.cs @@ -111,6 +111,7 @@ private double DragThumbVertical(Thumb? thumb, double min, double max, double ne var ttHeight = _toolTip.ActualHeight / 2; Canvas.SetTop(_toolTip, thumbCenter - ttHeight); + UpdateToolTipPositionForVertical(); } // Invert: top position (0) = Maximum, bottom position (DragWidth) = Minimum @@ -130,23 +131,33 @@ private void Thumb_DragStarted(Thumb thumb) if (_toolTip != null) { - _toolTip.Visibility = Visibility.Visible; - _toolTip.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity)); - - if (isHorizontal) + if (!isHorizontal && VerticalToolTipPlacement == VerticalToolTipPlacement.None) { - var thumbCenter = _absolutePosition + (thumb.Width / 2); - Canvas.SetLeft(_toolTip, thumbCenter - (_toolTip.ActualWidth / 2)); + _toolTip.Visibility = Visibility.Collapsed; } else { - var thumbCenter = _absolutePosition + (thumb.Height / 2); - Canvas.SetTop(_toolTip, thumbCenter - (_toolTip.ActualHeight / 2)); - } - - if (_toolTipText != null) - { - UpdateToolTipText(this, _toolTipText, useMin ? RangeStart : RangeEnd); + _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)); + + if (isHorizontal) + { + var thumbCenter = _absolutePosition + (thumb.Width / 2); + Canvas.SetLeft(_toolTip, thumbCenter - (_toolTip.DesiredSize.Width / 2)); + } + else + { + var thumbCenter = _absolutePosition + (thumb.Height / 2); + Canvas.SetTop(_toolTip, thumbCenter - (_toolTip.DesiredSize.Height / 2)); + UpdateToolTipPositionForVertical(); + } } } diff --git a/components/RangeSelector/src/RangeSelector.Properties.cs b/components/RangeSelector/src/RangeSelector.Properties.cs index a58dd193..9d75e075 100644 --- a/components/RangeSelector/src/RangeSelector.Properties.cs +++ b/components/RangeSelector/src/RangeSelector.Properties.cs @@ -69,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. /// @@ -141,6 +151,19 @@ public Orientation Orientation 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; 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 082f093f..55805d6d 100644 --- a/components/RangeSelector/src/RangeSelector.cs +++ b/components/RangeSelector/src/RangeSelector.cs @@ -329,4 +329,30 @@ private void RangeSelector_IsEnabledChanged(object sender, DependencyPropertyCha { VisualStateManager.GoToState(this, IsEnabled ? NormalState : DisabledState, true); } + + 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 dfd2af3b..456afb0f 100644 --- a/components/RangeSelector/src/RangeSelector.xaml +++ b/components/RangeSelector/src/RangeSelector.xaml @@ -262,7 +262,7 @@ - + From 69222d7a501e7951755e53e88f44cd7058d2642e Mon Sep 17 00:00:00 2001 From: Illustar0 Date: Fri, 5 Dec 2025 01:31:56 +0800 Subject: [PATCH 7/8] fix: use DesiredSize.Height for tooltip measurement Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- components/RangeSelector/src/RangeSelector.Input.Drag.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/RangeSelector/src/RangeSelector.Input.Drag.cs b/components/RangeSelector/src/RangeSelector.Input.Drag.cs index 0d4a65ed..ab84e06b 100644 --- a/components/RangeSelector/src/RangeSelector.Input.Drag.cs +++ b/components/RangeSelector/src/RangeSelector.Input.Drag.cs @@ -108,7 +108,7 @@ private double DragThumbVertical(Thumb? thumb, double min, double max, double ne { var thumbCenter = nextPos + (thumb.Height / 2); _toolTip.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity)); - var ttHeight = _toolTip.ActualHeight / 2; + var ttHeight = _toolTip.DesiredSize.Height / 2; Canvas.SetTop(_toolTip, thumbCenter - ttHeight); UpdateToolTipPositionForVertical(); From fa1323f7ef16fd5565e5d2e67e56e20788524f62 Mon Sep 17 00:00:00 2001 From: Illustar0 Date: Fri, 5 Dec 2025 01:47:28 +0800 Subject: [PATCH 8/8] fix: prevent tooltip from showing when vertical placement is not set --- components/RangeSelector/src/RangeSelector.Input.Key.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/components/RangeSelector/src/RangeSelector.Input.Key.cs b/components/RangeSelector/src/RangeSelector.Input.Key.cs index 75d1b957..eeb4c888 100644 --- a/components/RangeSelector/src/RangeSelector.Input.Key.cs +++ b/components/RangeSelector/src/RangeSelector.Input.Key.cs @@ -74,6 +74,8 @@ private void MaxThumb_KeyDown(object sender, KeyRoutedEventArgs e) private void ShowToolTip() { + var isHorizontal = Orientation == Orientation.Horizontal; + if (!isHorizontal && VerticalToolTipPlacement == VerticalToolTipPlacement.None) return; if (_toolTip != null) { _toolTip.Visibility = Visibility.Visible;