diff --git a/MainDemo.Wpf/RatingBar.xaml b/MainDemo.Wpf/RatingBar.xaml index 4290be8238..42109a3a6e 100644 --- a/MainDemo.Wpf/RatingBar.xaml +++ b/MainDemo.Wpf/RatingBar.xaml @@ -197,6 +197,55 @@ VerticalAlignment="Top" Text="{Binding ElementName=CustomRatingBarFractionalPreview, Path=Value, StringFormat=Rating: {0}}" /> + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/MaterialDesignThemes.Wpf.Tests/RatingBarTests.cs b/MaterialDesignThemes.Wpf.Tests/RatingBarTests.cs index a3016d6804..66925cd1ba 100644 --- a/MaterialDesignThemes.Wpf.Tests/RatingBarTests.cs +++ b/MaterialDesignThemes.Wpf.Tests/RatingBarTests.cs @@ -231,6 +231,29 @@ public void TextBlockForegroundConverter_ShouldReturnFractionalGradientStops_Whe Assert.Equal(brush.Color.WithAlphaChannel(RatingBar.TextBlockForegroundConverter.SemiTransparent), stop2.Color); } + [Fact] + public void TextBlockForegroundConverter_ShouldReturnFractionalGradientStops_WhenValueCovers10PercentOfButtonValueAndDirectionIsInverted() + { + // Arrange + SolidColorBrush brush = Brushes.Red; + IMultiValueConverter converter = RatingBar.TextBlockForegroundConverter.Instance; + object[] values = Arrange_TextBlockForegroundConverterValues(brush, value: 1.1, buttonValue: 2, invertDirection: true); + + // Act + var result = converter.Convert(values, typeof(Brush), null, CultureInfo.CurrentCulture) as Brush; + + // Assert + Assert.IsAssignableFrom(result); + LinearGradientBrush resultBrush = (LinearGradientBrush)result!; + Assert.Equal(2, resultBrush.GradientStops.Count); + GradientStop stop1 = resultBrush.GradientStops[0]; + GradientStop stop2 = resultBrush.GradientStops[1]; + Assert.Equal(0.9, stop1.Offset, 10); + Assert.Equal(brush.Color.WithAlphaChannel(RatingBar.TextBlockForegroundConverter.SemiTransparent), stop1.Color); + Assert.Equal(0.9, stop2.Offset, 10); + Assert.Equal(brush.Color, stop2.Color); + } + [Fact] public void TextBlockForegroundConverter_ShouldReturnFractionalGradientStops_WhenValueCovers42PercentOfButtonValue() { @@ -277,15 +300,15 @@ public void TextBlockForegroundConverter_ShouldReturnFractionalGradientStops_Whe Assert.Equal(brush.Color.WithAlphaChannel(RatingBar.TextBlockForegroundConverter.SemiTransparent), stop2.Color); } - private static object[] Arrange_TextBlockForegroundConverterValues(SolidColorBrush brush, double value, int buttonValue, Orientation orientation = Orientation.Horizontal) => - new object[] { brush, orientation, value, buttonValue }; + private static object[] Arrange_TextBlockForegroundConverterValues(SolidColorBrush brush, double value, int buttonValue, Orientation orientation = Orientation.Horizontal, bool invertDirection = false) => + new object[] { brush, orientation, invertDirection, value, buttonValue }; [Fact] public void PreviewIndicatorTransformXConverter_ShouldCenterPreviewIndicator_WhenFractionalValuesAreDisabledAndOrientationIsHorizontal() { // Arrange IMultiValueConverter converter = RatingBar.PreviewIndicatorTransformXConverter.Instance; - object[] values = Arrange_PreviewIndicatorTransformXConverterValues(100, 20, Orientation.Horizontal, false, 1, 1); + object[] values = Arrange_PreviewIndicatorTransformXConverterValues(100, 20, Orientation.Horizontal, false, false, 1, 1); // Act double? result = converter.Convert(values, typeof(double), null, CultureInfo.CurrentCulture) as double?; @@ -295,19 +318,21 @@ public void PreviewIndicatorTransformXConverter_ShouldCenterPreviewIndicator_Whe Assert.Equal(40.0, result); // 50% of 100 minus 20/2 } - [Fact] - public void PreviewIndicatorTransformXConverter_ShouldOffsetPreviewIndicatorByPercentage_WhenFractionalValuesAreEnabledAndOrientationIsHorizontal() + [Theory] + [InlineData(false, 15.0)] // 25% of 100 minus 20/2 + [InlineData(true, 65.0)] // 75% of 100 minus 20/2 + public void PreviewIndicatorTransformXConverter_ShouldOffsetPreviewIndicatorByPercentage_WhenFractionalValuesAreEnabledAndOrientationIsHorizontal(bool invertDirection, double expectedValue) { // Arrange IMultiValueConverter converter = RatingBar.PreviewIndicatorTransformXConverter.Instance; - object[] values = Arrange_PreviewIndicatorTransformXConverterValues(100, 20, Orientation.Horizontal, true, 1.25, 1); + object[] values = Arrange_PreviewIndicatorTransformXConverterValues(100, 20, Orientation.Horizontal, invertDirection, true, 1.25, 1); // Act double? result = converter.Convert(values, typeof(double), null, CultureInfo.CurrentCulture) as double?; // Assert Assert.NotNull(result); - Assert.Equal(15.0, result); // 25% of 100 minus 20/2 + Assert.Equal(expectedValue, result); } [Fact] @@ -315,7 +340,7 @@ public void PreviewIndicatorTransformXConverter_ShouldPlacePreviewIndicatorWithS { // Arrange IMultiValueConverter converter = RatingBar.PreviewIndicatorTransformXConverter.Instance; - object[] values = Arrange_PreviewIndicatorTransformXConverterValues(100, 20, Orientation.Vertical, false, 1, 1); + object[] values = Arrange_PreviewIndicatorTransformXConverterValues(100, 20, Orientation.Vertical, false, false, 1, 1); double expectedValue = -20 - RatingBar.PreviewIndicatorTransformXConverter.Margin; // Act @@ -331,7 +356,7 @@ public void PreviewIndicatorTransformXConverter_ShouldPlacePreviewIndicatorWithS { // Arrange IMultiValueConverter converter = RatingBar.PreviewIndicatorTransformXConverter.Instance; - object[] values = Arrange_PreviewIndicatorTransformXConverterValues(100, 20, Orientation.Vertical, true, 1.25, 1); + object[] values = Arrange_PreviewIndicatorTransformXConverterValues(100, 20, Orientation.Vertical, false, true, 1.25, 1); double expectedValue = -20 - RatingBar.PreviewIndicatorTransformXConverter.Margin; // Act @@ -344,15 +369,15 @@ public void PreviewIndicatorTransformXConverter_ShouldPlacePreviewIndicatorWithS - private static object[] Arrange_PreviewIndicatorTransformXConverterValues(double ratingBarButtonActualWidth, double previewValueActualWidth, Orientation orientation, bool isFractionalValueEnabled, double previewValue, int buttonValue) => - new object[] { ratingBarButtonActualWidth, previewValueActualWidth, orientation, isFractionalValueEnabled, previewValue, buttonValue }; + private static object[] Arrange_PreviewIndicatorTransformXConverterValues(double ratingBarButtonActualWidth, double previewValueActualWidth, Orientation orientation, bool invertDirection, bool isFractionalValueEnabled, double previewValue, int buttonValue) => + new object[] { ratingBarButtonActualWidth, previewValueActualWidth, orientation, invertDirection, isFractionalValueEnabled, previewValue, buttonValue }; [Fact] public void PreviewIndicatorTransformYConverter_ShouldPlacePreviewIndicatorWithSmallMargin_WhenFractionalValuesAreDisabledAndOrientationIsHorizontal() { // Arrange IMultiValueConverter converter = RatingBar.PreviewIndicatorTransformYConverter.Instance; - object[] values = Arrange_PreviewIndicatorTransformYConverterValues(100, 20, Orientation.Horizontal, false, 1, 1); + object[] values = Arrange_PreviewIndicatorTransformYConverterValues(100, 20, Orientation.Horizontal, false, false, 1, 1); double expectedValue = -20 - RatingBar.PreviewIndicatorTransformYConverter.Margin; // Act @@ -368,7 +393,7 @@ public void PreviewIndicatorTransformYConverter_ShouldPlacePreviewIndicatorWithS { // Arrange IMultiValueConverter converter = RatingBar.PreviewIndicatorTransformYConverter.Instance; - object[] values = Arrange_PreviewIndicatorTransformYConverterValues(100, 20, Orientation.Horizontal, true, 1.25, 1); + object[] values = Arrange_PreviewIndicatorTransformYConverterValues(100, 20, Orientation.Horizontal, false, true, 1.25, 1); double expectedValue = -20 - RatingBar.PreviewIndicatorTransformYConverter.Margin; // Act @@ -384,7 +409,7 @@ public void PreviewIndicatorTransformYConverter_ShouldCenterPreviewIndicator_Whe { // Arrange IMultiValueConverter converter = RatingBar.PreviewIndicatorTransformYConverter.Instance; - object[] values = Arrange_PreviewIndicatorTransformYConverterValues(100, 20, Orientation.Vertical, false, 1, 1); + object[] values = Arrange_PreviewIndicatorTransformYConverterValues(100, 20, Orientation.Vertical, false, false, 1, 1); // Act double? result = converter.Convert(values, typeof(double), null, CultureInfo.CurrentCulture) as double?; @@ -394,23 +419,25 @@ public void PreviewIndicatorTransformYConverter_ShouldCenterPreviewIndicator_Whe Assert.Equal(40.0, result); // 50% of 100 minus 20/2 } - [Fact] - public void PreviewIndicatorTransformYConverter_ShouldPreviewIndicatorByPercentage_WhenFractionalValuesAreEnabledAndOrientationIsVertical() + [Theory] + [InlineData(false, 15.0)] // 25% of 100 minus 20/2 + [InlineData(true, 65.0)] // 75% of 100 minus 20/2 + public void PreviewIndicatorTransformYConverter_ShouldPreviewIndicatorByPercentage_WhenFractionalValuesAreEnabledAndOrientationIsVertical(bool invertDirection, double expectedValue) { // Arrange IMultiValueConverter converter = RatingBar.PreviewIndicatorTransformYConverter.Instance; - object[] values = Arrange_PreviewIndicatorTransformYConverterValues(100, 20, Orientation.Vertical, true, 1.25, 1); + object[] values = Arrange_PreviewIndicatorTransformYConverterValues(100, 20, Orientation.Vertical, invertDirection, true, 1.25, 1); // Act double? result = converter.Convert(values, typeof(double), null, CultureInfo.CurrentCulture) as double?; // Assert Assert.NotNull(result); - Assert.Equal(15.0, result); // 25% of 100 minus 20/2 + Assert.Equal(expectedValue, result); } - private static object[] Arrange_PreviewIndicatorTransformYConverterValues(double ratingBarButtonActualHeight, double previewValueActualHeight, Orientation orientation, bool isFractionalValueEnabled, double previewValue, int buttonValue) => - new object[] { ratingBarButtonActualHeight, previewValueActualHeight, orientation, isFractionalValueEnabled, previewValue, buttonValue }; + private static object[] Arrange_PreviewIndicatorTransformYConverterValues(double ratingBarButtonActualHeight, double previewValueActualHeight, Orientation orientation, bool invertDirection, bool isFractionalValueEnabled, double previewValue, int buttonValue) => + new object[] { ratingBarButtonActualHeight, previewValueActualHeight, orientation, invertDirection, isFractionalValueEnabled, previewValue, buttonValue }; } internal static class ColorExtensions diff --git a/MaterialDesignThemes.Wpf/RatingBar.cs b/MaterialDesignThemes.Wpf/RatingBar.cs index e5ce3246fc..1f3e033f43 100644 --- a/MaterialDesignThemes.Wpf/RatingBar.cs +++ b/MaterialDesignThemes.Wpf/RatingBar.cs @@ -45,7 +45,29 @@ private double GetValueAtMousePosition(RatingBarButton ratingBarButton) { // Get mouse offset inside source Point p = Mouse.GetPosition(ratingBarButton); - double percentSelected = Orientation == Orientation.Horizontal ? p.X / ratingBarButton.ActualWidth : p.Y / ratingBarButton.ActualHeight; + double percentSelected; + switch (Orientation) + { + case Orientation.Horizontal: + if (InvertDirection) + { + percentSelected = 1 - (p.X / ratingBarButton.ActualWidth); + break; + } + percentSelected = p.X / ratingBarButton.ActualWidth; + break; + case Orientation.Vertical: + if (InvertDirection) + { + percentSelected = 1 - (p.Y / ratingBarButton.ActualHeight); + break; + } + percentSelected = p.Y / ratingBarButton.ActualHeight; + break; + default: + throw new ArgumentOutOfRangeException(); + } + return ratingBarButton.Value - 1 + percentSelected; } @@ -243,6 +265,15 @@ public Orientation Orientation set => SetValue(OrientationProperty, value); } + public static readonly DependencyProperty InvertDirectionProperty = DependencyProperty.Register( + nameof(InvertDirection), typeof(bool), typeof(RatingBar), new PropertyMetadata(default(bool))); + + public bool InvertDirection + { + get => (bool) GetValue(InvertDirectionProperty); + set => SetValue(InvertDirectionProperty, value); + } + public static readonly DependencyProperty IsReadOnlyProperty = DependencyProperty.Register( nameof(IsReadOnly), typeof(bool), typeof(RatingBar), new PropertyMetadata(default(bool))); @@ -289,21 +320,44 @@ private void RebuildButtons() // When fractional values are enabled, the first rating button represents the value Min when not selected at all and Min+1 when fully selected; // thus we start with the value Min+1 for the values of the rating buttons. int start = IsFractionalValueEnabled ? Min + 1 : Min; - for (int i = start; i <= Max; i++) + + if (InvertDirection) { - var ratingBarButton = new RatingBarButton + for (int i = Max; i >= start; i--) { - Content = i, - ContentTemplate = ValueItemTemplate, - ContentTemplateSelector = ValueItemTemplateSelector, + var ratingBarButton = new RatingBarButton + { + Content = i, + ContentTemplate = ValueItemTemplate, + ContentTemplateSelector = ValueItemTemplateSelector, #pragma warning disable CS0618 // Type or member is obsolete - IsWithinSelectedValue = i <= Value, + IsWithinSelectedValue = i <= Value, #pragma warning restore CS0618 // Type or member is obsolete - Style = ValueItemContainerButtonStyle, - Value = i, - }; - ratingBarButton.MouseMove += RatingBarButton_MouseMove; - _ratingButtonsInternal.Add(ratingBarButton); + Style = ValueItemContainerButtonStyle, + Value = i, + }; + ratingBarButton.MouseMove += RatingBarButton_MouseMove; + _ratingButtonsInternal.Add(ratingBarButton); + } + } + else + { + for (int i = start; i <= Max; i++) + { + var ratingBarButton = new RatingBarButton + { + Content = i, + ContentTemplate = ValueItemTemplate, + ContentTemplateSelector = ValueItemTemplateSelector, +#pragma warning disable CS0618 // Type or member is obsolete + IsWithinSelectedValue = i <= Value, +#pragma warning restore CS0618 // Type or member is obsolete + Style = ValueItemContainerButtonStyle, + Value = i, + }; + ratingBarButton.MouseMove += RatingBarButton_MouseMove; + _ratingButtonsInternal.Add(ratingBarButton); + } } } @@ -333,11 +387,12 @@ internal class TextBlockForegroundConverter : IMultiValueConverter public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture) { - if (values?.Length == 4 + if (values?.Length == 5 && values[0] is SolidColorBrush brush && values[1] is Orientation orientation - && values[2] is double value - && values[3] is int buttonValue) + && values[2] is bool invertDirection + && values[3] is double value + && values[4] is int buttonValue) { if (value >= buttonValue) return brush; @@ -348,20 +403,38 @@ public object Convert(object[] values, Type targetType, object parameter, Cultur if (value > buttonValue - 1.0) { double offset = value - buttonValue + 1; + if (invertDirection) + { + offset = 1 - offset; + } return new LinearGradientBrush { StartPoint = orientation == Orientation.Horizontal ? new Point(0, 0.5) : new Point(0.5, 0), EndPoint = orientation == Orientation.Horizontal ? new Point(1, 0.5) : new Point(0.5, 1), - GradientStops = new() - { - new GradientStop { Color = originalColor, Offset = offset }, - new GradientStop { Color = semiTransparent, Offset = offset } - } + + GradientStops = CreateGradientStopCollection(originalColor, semiTransparent, offset, invertDirection) }; } return new SolidColorBrush(semiTransparent); } + GradientStopCollection CreateGradientStopCollection(Color originalColor, Color semiTransparent, double offset, bool invertDirection) + { + if (invertDirection) + { + return new() + { + new GradientStop {Color = semiTransparent, Offset = offset}, + new GradientStop {Color = originalColor, Offset = offset}, + }; + } + return new() + { + new GradientStop {Color = originalColor, Offset = offset}, + new GradientStop {Color = semiTransparent, Offset = offset} + }; + } + // This should never happen (returning actual brush to avoid the compilers squiggly line warning) return Brushes.Transparent; } @@ -377,13 +450,14 @@ internal class PreviewIndicatorTransformXConverter : IMultiValueConverter public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture) { - if (values.Length >= 6 + if (values.Length >= 7 && values[0] is double ratingBarButtonActualWidth && values[1] is double previewValueActualWidth && values[2] is Orientation ratingBarOrientation - && values[3] is bool isFractionalValueEnabled - && values[4] is double previewValue - && values[5] is int ratingButtonValue) + && values[3] is bool ratingBarInvertDirection + && values[4] is bool isFractionalValueEnabled + && values[5] is double previewValue + && values[6] is int ratingButtonValue) { if (!isFractionalValueEnabled) { @@ -404,7 +478,10 @@ public object Convert(object[] values, Type targetType, object parameter, Cultur return ratingBarOrientation switch { - Orientation.Horizontal => percent * ratingBarButtonActualWidth - (previewValueActualWidth / 2), + Orientation.Horizontal => ratingBarInvertDirection ? + (1 - percent) * ratingBarButtonActualWidth - (previewValueActualWidth / 2) : + percent * ratingBarButtonActualWidth - (previewValueActualWidth / 2) + , Orientation.Vertical => -previewValueActualWidth - Margin, _ => throw new ArgumentOutOfRangeException() }; @@ -423,13 +500,14 @@ internal class PreviewIndicatorTransformYConverter : IMultiValueConverter public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture) { - if (values.Length >= 6 + if (values.Length >= 7 && values[0] is double ratingBarButtonActualHeight && values[1] is double previewValueActualHeight && values[2] is Orientation ratingBarOrientation - && values[3] is bool isFractionalValueEnabled - && values[4] is double previewValue - && values[5] is int ratingButtonValue) + && values[3] is bool ratingBarInvertDirection + && values[4] is bool isFractionalValueEnabled + && values[5] is double previewValue + && values[6] is int ratingButtonValue) { if (!isFractionalValueEnabled) { @@ -451,7 +529,9 @@ public object Convert(object[] values, Type targetType, object parameter, Cultur return ratingBarOrientation switch { Orientation.Horizontal => -previewValueActualHeight - Margin, - Orientation.Vertical => percent * ratingBarButtonActualHeight - (previewValueActualHeight / 2), + Orientation.Vertical => ratingBarInvertDirection ? + (1 - percent) * ratingBarButtonActualHeight - (previewValueActualHeight / 2) : + percent * ratingBarButtonActualHeight - (previewValueActualHeight / 2), _ => throw new ArgumentOutOfRangeException() }; } diff --git a/MaterialDesignThemes.Wpf/Themes/MaterialDesignTheme.RatingBar.xaml b/MaterialDesignThemes.Wpf/Themes/MaterialDesignTheme.RatingBar.xaml index 0d16fcb538..389cca72f5 100644 --- a/MaterialDesignThemes.Wpf/Themes/MaterialDesignTheme.RatingBar.xaml +++ b/MaterialDesignThemes.Wpf/Themes/MaterialDesignTheme.RatingBar.xaml @@ -49,6 +49,7 @@ + @@ -70,6 +71,7 @@ + @@ -81,6 +83,7 @@ + @@ -124,6 +127,7 @@ + @@ -135,6 +139,7 @@ + @@ -245,6 +250,7 @@ + @@ -278,4 +284,4 @@ - \ No newline at end of file +