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 @@