From d133c3172a445b67748a5037c8559adb64b8ca32 Mon Sep 17 00:00:00 2001 From: Stephen Quan Date: Fri, 28 Nov 2025 16:16:09 +1100 Subject: [PATCH 01/19] Initial RangeSlider implementation --- .../AppShell.xaml.cs | 1 + .../MauiProgram.cs | 1 + .../Views/RangeSlider/RangeSliderPage.xaml | 149 ++++++ .../Views/RangeSlider/RangeSliderPage.xaml.cs | 13 + .../ViewModels/Views/RangeSliderViewModel.cs | 36 ++ .../ViewModels/Views/ViewsGalleryViewModel.cs | 1 + .../Defaults/RangeSliderDefaults.shared.cs | 52 +++ .../Primitives/RangeSliderFocusMode.cs | 12 + .../Views/RangeSlider/RangeSlider.shared.cs | 433 ++++++++++++++++++ 9 files changed, 698 insertions(+) create mode 100644 samples/CommunityToolkit.Maui.Sample/Pages/Views/RangeSlider/RangeSliderPage.xaml create mode 100644 samples/CommunityToolkit.Maui.Sample/Pages/Views/RangeSlider/RangeSliderPage.xaml.cs create mode 100644 samples/CommunityToolkit.Maui.Sample/ViewModels/Views/RangeSliderViewModel.cs create mode 100644 src/CommunityToolkit.Maui.Core/Primitives/Defaults/RangeSliderDefaults.shared.cs create mode 100644 src/CommunityToolkit.Maui.Core/Primitives/RangeSliderFocusMode.cs create mode 100644 src/CommunityToolkit.Maui/Views/RangeSlider/RangeSlider.shared.cs diff --git a/samples/CommunityToolkit.Maui.Sample/AppShell.xaml.cs b/samples/CommunityToolkit.Maui.Sample/AppShell.xaml.cs index 12721c1b46..5f1022318e 100644 --- a/samples/CommunityToolkit.Maui.Sample/AppShell.xaml.cs +++ b/samples/CommunityToolkit.Maui.Sample/AppShell.xaml.cs @@ -135,6 +135,7 @@ public partial class AppShell : Shell CreateViewModelMapping(), CreateViewModelMapping(), CreateViewModelMapping(), + CreateViewModelMapping(), CreateViewModelMapping(), CreateViewModelMapping(), CreateViewModelMapping(), diff --git a/samples/CommunityToolkit.Maui.Sample/MauiProgram.cs b/samples/CommunityToolkit.Maui.Sample/MauiProgram.cs index 24cc21bcb2..8638508867 100644 --- a/samples/CommunityToolkit.Maui.Sample/MauiProgram.cs +++ b/samples/CommunityToolkit.Maui.Sample/MauiProgram.cs @@ -258,6 +258,7 @@ static void RegisterViewsAndViewModels(in IServiceCollection services) services.AddTransientWithShellRoute(); services.AddTransientWithShellRoute(); services.AddTransientWithShellRoute(); + services.AddTransientWithShellRoute(); services.AddTransientWithShellRoute(); services.AddTransientWithShellRoute(); services.AddTransientWithShellRoute(); diff --git a/samples/CommunityToolkit.Maui.Sample/Pages/Views/RangeSlider/RangeSliderPage.xaml b/samples/CommunityToolkit.Maui.Sample/Pages/Views/RangeSlider/RangeSliderPage.xaml new file mode 100644 index 0000000000..2557211a7e --- /dev/null +++ b/samples/CommunityToolkit.Maui.Sample/Pages/Views/RangeSlider/RangeSliderPage.xaml @@ -0,0 +1,149 @@ + + + + + + + + + + + + + + + diff --git a/samples/CommunityToolkit.Maui.Sample/Pages/Views/RangeSlider/RangeSliderPage.xaml.cs b/samples/CommunityToolkit.Maui.Sample/Pages/Views/RangeSlider/RangeSliderPage.xaml.cs new file mode 100644 index 0000000000..013946a22a --- /dev/null +++ b/samples/CommunityToolkit.Maui.Sample/Pages/Views/RangeSlider/RangeSliderPage.xaml.cs @@ -0,0 +1,13 @@ +using System.Diagnostics.CodeAnalysis; +using CommunityToolkit.Maui.Alerts; +using CommunityToolkit.Maui.Sample.ViewModels.Views; + +namespace CommunityToolkit.Maui.Sample.Pages.Views; + +public partial class RangeSliderPage : BasePage +{ + public RangeSliderPage(RangeSliderViewModel viewModel) : base(viewModel) + { + InitializeComponent(); + } +} \ No newline at end of file diff --git a/samples/CommunityToolkit.Maui.Sample/ViewModels/Views/RangeSliderViewModel.cs b/samples/CommunityToolkit.Maui.Sample/ViewModels/Views/RangeSliderViewModel.cs new file mode 100644 index 0000000000..e7767ead09 --- /dev/null +++ b/samples/CommunityToolkit.Maui.Sample/ViewModels/Views/RangeSliderViewModel.cs @@ -0,0 +1,36 @@ +using CommunityToolkit.Mvvm.ComponentModel; + +namespace CommunityToolkit.Maui.Sample.ViewModels.Views; + +public partial class RangeSliderViewModel : BaseViewModel +{ + [ObservableProperty] + public partial double SimpleLowerValue { get; set; } = 125; + + [ObservableProperty] + public partial double SimpleUpperValue { get; set; } = 175; + + [ObservableProperty] + public partial double LowerValueWithCustomStyle { get; set; } = 125; + + [ObservableProperty] + public partial double UpperValueWithCustomStyle { get; set; } = 175; + + [ObservableProperty] + public partial double LowerValueWithStep { get; set; } = 125; + + [ObservableProperty] + public partial double UpperValueWithStep { get; set; } = 175; + + [ObservableProperty] + public partial double LowerValueDescending { get; set; } = 175; + + [ObservableProperty] + public partial double UpperValueDescending { get; set; } = 125; + + [ObservableProperty] + public partial double LowerValueDescendingWithStep { get; set; } = 175; + + [ObservableProperty] + public partial double UpperValueDescendingWithStep { get; set; } = 125; +} \ No newline at end of file diff --git a/samples/CommunityToolkit.Maui.Sample/ViewModels/Views/ViewsGalleryViewModel.cs b/samples/CommunityToolkit.Maui.Sample/ViewModels/Views/ViewsGalleryViewModel.cs index ea038112a0..e02cb17fe0 100644 --- a/samples/CommunityToolkit.Maui.Sample/ViewModels/Views/ViewsGalleryViewModel.cs +++ b/samples/CommunityToolkit.Maui.Sample/ViewModels/Views/ViewsGalleryViewModel.cs @@ -25,6 +25,7 @@ public sealed partial class ViewsGalleryViewModel() : BaseGalleryViewModel( SectionModel.Create("MediaElement in CarouselView", Colors.Red, "MediaElement can be used inside a DataTemplate in a CarouselView"), SectionModel.Create("MediaElement in CollectionView", Colors.Red, "MediaElement can be used inside a DataTemplate in a CollectionView"), SectionModel.Create("MediaElement in a Multi-Window Application", Colors.Red, "Demonstrates that MediaElement can be used inside a DataTemplate simultaneously on multiple windows"), + SectionModel.Create("RangeSlider Page", Colors.Red, "A page demonstrating RangeSlider in various styles."), SectionModel.Create("RatingView Showcase Page", Colors.Red, "A page with showcase examples for the RatingView control."), SectionModel.Create("RatingView XAML Page", Colors.Red, "A page demonstrating the RatingView control and possible uses using XAML"), SectionModel.Create("RatingView C# Page", Colors.Red, "A page demonstrating the RatingView control and possible uses using C#"), diff --git a/src/CommunityToolkit.Maui.Core/Primitives/Defaults/RangeSliderDefaults.shared.cs b/src/CommunityToolkit.Maui.Core/Primitives/Defaults/RangeSliderDefaults.shared.cs new file mode 100644 index 0000000000..7b8c2a486b --- /dev/null +++ b/src/CommunityToolkit.Maui.Core/Primitives/Defaults/RangeSliderDefaults.shared.cs @@ -0,0 +1,52 @@ +namespace CommunityToolkit.Maui.Core; + +/// +/// Default Values for RangeSlider /> +/// +public static class RangeSliderDefaults +{ + /// Default width request. + public const double WidthRequest = 200; + + /// Default for Minimum Value + public const double MinimumValue = 0; + + /// Default for Maximum Value + public const double MaximumValue = 1; + + /// Default for Lower Value + public const double LowerValue = 0; + + /// Default for Upper Value + public const double UpperValue = 1; + + /// Default for Step Size + public const double StepSize = 0; + + /// Default value for Lower Thumb Color + public static Color LowerThumbColor { get; } = Colors.Gray; + + /// Default value for Upper Thumb Color + public static Color UpperThumbColor { get; } = Colors.Gray; + + /// Default value for Track Color + public static Color InnerTrackColor { get; } = Colors.Pink; + + /// Default value for Track Size + public const double InnerTrackSize = 6; + + /// Default value for Inner Track Radius + public static CornerRadius InnerTrackCornerRadius = 3; + + /// Default value for Outer Track Color + public static Color OuterTrackColor { get; } = Colors.LightGray; + + /// Default value for Outer Track Size + public const double OuterTrackSize = 6; + + /// Default value for Outer Track Radius + public static CornerRadius OuterTrackCornerRadius = 3; + + /// Default value for Focus Mode + public const RangeSliderFocusMode FocusMode = RangeSliderFocusMode.Default; +} \ No newline at end of file diff --git a/src/CommunityToolkit.Maui.Core/Primitives/RangeSliderFocusMode.cs b/src/CommunityToolkit.Maui.Core/Primitives/RangeSliderFocusMode.cs new file mode 100644 index 0000000000..5f7918b7e7 --- /dev/null +++ b/src/CommunityToolkit.Maui.Core/Primitives/RangeSliderFocusMode.cs @@ -0,0 +1,12 @@ +namespace CommunityToolkit.Maui.Core; + +/// Enum for RangeSlider focus mode +public enum RangeSliderFocusMode +{ + /// Balance the focus between the lower and upper thumbs + Default, + /// The lower thumb has focus + Lower, + /// The upper thumb has focus + Upper, +} \ No newline at end of file diff --git a/src/CommunityToolkit.Maui/Views/RangeSlider/RangeSlider.shared.cs b/src/CommunityToolkit.Maui/Views/RangeSlider/RangeSlider.shared.cs new file mode 100644 index 0000000000..55184c9c21 --- /dev/null +++ b/src/CommunityToolkit.Maui/Views/RangeSlider/RangeSlider.shared.cs @@ -0,0 +1,433 @@ +using System.ComponentModel; +using CommunityToolkit.Maui.Core; +using Microsoft.Maui.Controls.Shapes; + +namespace CommunityToolkit.Maui.Views; + +/// RangeSlider control. +public partial class RangeSlider : ContentView +{ + /// The backing store for the bindable property. + public static readonly BindableProperty MinimumValueProperty = BindableProperty.Create(nameof(MinimumValue), typeof(double), typeof(RangeSlider), defaultValue: RangeSliderDefaults.MinimumValue); + + /// The backing store for the bindable property. + public static readonly BindableProperty MaximumValueProperty = BindableProperty.Create(nameof(MaximumValue), typeof(double), typeof(RangeSlider), defaultValue: RangeSliderDefaults.MaximumValue); + + /// The backing store for the bindable property. + public static readonly BindableProperty LowerValueProperty = BindableProperty.Create(nameof(LowerValue), typeof(double), typeof(RangeSlider), defaultValue: RangeSliderDefaults.LowerValue, coerceValue: CoerceLowerValue); + + /// The backing store for the bindable property. + public static readonly BindableProperty UpperValueProperty = BindableProperty.Create(nameof(UpperValue), typeof(double), typeof(RangeSlider), defaultValue: RangeSliderDefaults.UpperValue, coerceValue: CoerceUpperValue); + + /// The backing store for the bindable property. + public static readonly BindableProperty StepSizeProperty = BindableProperty.Create(nameof(StepSize), typeof(double), typeof(RangeSlider), defaultValue: RangeSliderDefaults.StepSize); + + /// The backing store for the bindable property. + public static readonly BindableProperty LowerThumbColorProperty = BindableProperty.Create(nameof(LowerThumbColor), typeof(Color), typeof(RangeSlider), defaultValue: RangeSliderDefaults.LowerThumbColor); + + /// The backing store for the bindable property. + public static readonly BindableProperty UpperThumbColorProperty = BindableProperty.Create(nameof(UpperThumbColor), typeof(Color), typeof(RangeSlider), defaultValue: RangeSliderDefaults.UpperThumbColor); + + /// The backing store for the bindable property. + public static readonly BindableProperty InnerTrackColorProperty = BindableProperty.Create(nameof(InnerTrackColor), typeof(Color), typeof(RangeSlider), defaultValue: RangeSliderDefaults.InnerTrackColor); + + /// The backing store for the bindable property. + public static readonly BindableProperty InnerTrackSizeProperty = BindableProperty.Create(nameof(InnerTrackSize), typeof(double), typeof(RangeSlider), defaultValue: RangeSliderDefaults.InnerTrackSize); + + /// The backing store for the bindable property + public static readonly BindableProperty InnerTrackCornerRadiusProperty = BindableProperty.Create(nameof(InnerTrackCornerRadius), typeof(CornerRadius), typeof(RangeSlider), defaultValue: RangeSliderDefaults.InnerTrackCornerRadius); + + /// The backing store for the bindable property. + public static readonly BindableProperty OuterTrackColorProperty = BindableProperty.Create(nameof(OuterTrackColor), typeof(Color), typeof(RangeSlider), defaultValue: RangeSliderDefaults.OuterTrackColor); + + /// The backing store for the bindable property. + public static readonly BindableProperty OuterTrackSizeProperty = BindableProperty.Create(nameof(OuterTrackSize), typeof(double), typeof(RangeSlider), defaultValue: RangeSliderDefaults.OuterTrackSize); + + /// The backing store for the bindable property + public static readonly BindableProperty OuterTrackCornerRadiusProperty = BindableProperty.Create(nameof(OuterTrackCornerRadius), typeof(CornerRadius), typeof(RangeSlider), defaultValue: RangeSliderDefaults.OuterTrackCornerRadius); + + /// The backing store for the bindable property. + public static readonly BindableProperty FocusModeProperty = BindableProperty.Create(nameof(FocusMode), typeof(RangeSliderFocusMode), typeof(RangeSlider), defaultValue: RangeSliderDefaults.FocusMode); + + readonly RoundRectangle outerTrack = new() + { + HorizontalOptions = LayoutOptions.Start, + VerticalOptions = LayoutOptions.Center, + }; + + readonly RoundRectangle innerTrack = new() + { + HorizontalOptions = LayoutOptions.Start, + VerticalOptions = LayoutOptions.Center, + }; + + readonly Slider lowerSlider = new() + { + HorizontalOptions = LayoutOptions.Start, + VerticalOptions = LayoutOptions.Center, + BackgroundColor = Colors.Transparent, + MinimumTrackColor = Colors.Transparent, + MaximumTrackColor = Colors.Transparent, + }; + + readonly Slider upperSlider = new() + { + HorizontalOptions = LayoutOptions.End, + VerticalOptions = LayoutOptions.Center, + BackgroundColor = Colors.Transparent, + MinimumTrackColor = Colors.Transparent, + MaximumTrackColor = Colors.Transparent, + }; + + bool clampValues = false; + + /// Initializes a new instance of the class. + public RangeSlider() + { + PropertyChanged += HandlePropertyChanged; + + WidthRequest = RangeSliderDefaults.WidthRequest; + outerTrack.SetBinding(RoundRectangle.HeightRequestProperty, BindingBase.Create(static p => p.OuterTrackSize, BindingMode.OneWay, source: this)); + outerTrack.SetBinding(RoundRectangle.BackgroundColorProperty, BindingBase.Create(static p => p.OuterTrackColor, BindingMode.OneWay, source: this)); + outerTrack.SetBinding(RoundRectangle.CornerRadiusProperty, BindingBase.Create(static p => p.OuterTrackCornerRadius, BindingMode.OneWay, source: this)); + innerTrack.SetBinding(RoundRectangle.HeightRequestProperty, BindingBase.Create(static p => p.InnerTrackSize, BindingMode.OneWay, source: this)); + innerTrack.SetBinding(RoundRectangle.BackgroundColorProperty, BindingBase.Create(static p => p.InnerTrackColor, BindingMode.OneWay, source: this)); + innerTrack.SetBinding(RoundRectangle.CornerRadiusProperty, BindingBase.Create(static p => p.InnerTrackCornerRadius, BindingMode.OneWay, source: this)); + lowerSlider.SetBinding(Slider.ThumbColorProperty, BindingBase.Create(static p => p.LowerThumbColor, BindingMode.OneWay, source: this)); + upperSlider.SetBinding(Slider.ThumbColorProperty, BindingBase.Create(static p => p.UpperThumbColor, BindingMode.OneWay, source: this)); + + Content = new Grid + { + Children = + { + outerTrack, + innerTrack, + lowerSlider, + upperSlider, + }, + }; + + Dispatcher.Dispatch(() => + { + clampValues = true; + CoerceValue(LowerValueProperty); + CoerceValue(UpperValueProperty); + lowerSlider.DragStarted += HandleLowerSliderDragStarted; + lowerSlider.DragCompleted += HandleLowerSliderDragCompleted; + lowerSlider.PropertyChanged += HandleLowerSliderPropertyChanged; + lowerSlider.Focused += HandlerLowerSliderFocused; + upperSlider.DragStarted += HandleUpperSliderDragStarted; + upperSlider.DragCompleted += HandleUpperSliderDragCompleted; + upperSlider.PropertyChanged += HandleUpperSliderPropertyChanged; + upperSlider.Focused += HandlerUpperSliderFocused; + UpdateSliderRanges(); + UpdateLowerSliderValue(); + UpdateUpperSliderValue(); + UpdateFocusedSliderLayout(); + UpdateOuterTrackLayout(); + UpdateInnerTrackLayout(); + }); + } + + static object CoerceLowerValue(BindableObject bindable, object value) + { + var rangeSlider = (RangeSlider)bindable; + var input = (double)value; + + if (rangeSlider.clampValues) + { + if (rangeSlider.MinimumValue <= rangeSlider.MaximumValue) + { + input = Math.Min(Math.Clamp(input, rangeSlider.MinimumValue, rangeSlider.MaximumValue), rangeSlider.UpperValue); + } + else + { + input = Math.Max(Math.Clamp(input, rangeSlider.MaximumValue, rangeSlider.MinimumValue), rangeSlider.UpperValue); + } + } + + return input; + } + + static object CoerceUpperValue(BindableObject bindable, object value) + { + var rangeSlider = (RangeSlider)bindable; + var input = (double)value; + + if (rangeSlider.clampValues) + { + if (rangeSlider.MinimumValue <= rangeSlider.MaximumValue) + { + input = Math.Max(Math.Clamp(input, rangeSlider.MinimumValue, rangeSlider.MaximumValue), rangeSlider.LowerValue); + } + else + { + input = Math.Min(Math.Clamp(input, rangeSlider.MaximumValue, rangeSlider.MinimumValue), rangeSlider.LowerValue); + } + } + + return input; + } + + void HandlePropertyChanged(object? sender, PropertyChangedEventArgs e) + { + if (e.PropertyName == MinimumValueProperty.PropertyName + || e.PropertyName == MaximumValueProperty.PropertyName + || e.PropertyName == StepSizeProperty.PropertyName) + { + UpdateSliderRanges(); + } + + if (e.PropertyName == WidthProperty.PropertyName + || e.PropertyName == MinimumValueProperty.PropertyName + || e.PropertyName == MaximumValueProperty.PropertyName + || e.PropertyName == StepSizeProperty.PropertyName) + { + UpdateFocusedSliderLayout(); + } + + if (e.PropertyName == LowerValueProperty.PropertyName) + { + UpdateLowerSliderValue(); + } + + if (e.PropertyName == UpperValueProperty.PropertyName) + { + UpdateUpperSliderValue(); + } + + if (e.PropertyName == WidthProperty.PropertyName + || e.PropertyName == MinimumValueProperty.PropertyName + || e.PropertyName == MaximumValueProperty.PropertyName + || e.PropertyName == OuterTrackSizeProperty.PropertyName) + { + UpdateOuterTrackLayout(); + } + + if (e.PropertyName == WidthProperty.PropertyName + || e.PropertyName == MinimumValueProperty.PropertyName + || e.PropertyName == MaximumValueProperty.PropertyName + || e.PropertyName == LowerValueProperty.PropertyName + || e.PropertyName == UpperValueProperty.PropertyName + || e.PropertyName == InnerTrackSizeProperty.PropertyName) + { + UpdateInnerTrackLayout(); + } + } + + void HandleLowerSliderDragStarted(object? sender, EventArgs e) + { + FocusMode = RangeSliderFocusMode.Lower; + UpdateFocusedSliderLayout(); + } + + void HandleLowerSliderDragCompleted(object? sender, EventArgs e) + { + FocusMode = RangeSliderFocusMode.Default; + UpdateFocusedSliderLayout(); + } + + void HandleLowerSliderPropertyChanged(object? sender, PropertyChangedEventArgs e) + { + if (e.PropertyName == Slider.ValueProperty.PropertyName) + { + LowerValue = (StepSize == 0.0 ? lowerSlider.Value : Math.Round(lowerSlider.Value)) * GetUnit() + MinimumValue; + FocusMode = RangeSliderFocusMode.Lower; + UpdateInnerTrackLayout(); + } + } + + void HandlerLowerSliderFocused(object? sender, FocusEventArgs e) + { + FocusMode = RangeSliderFocusMode.Lower; + UpdateFocusedSliderLayout(); + } + + void HandleUpperSliderDragStarted(object? sender, EventArgs e) + { + FocusMode = RangeSliderFocusMode.Upper; + UpdateFocusedSliderLayout(); + } + + void HandleUpperSliderDragCompleted(object? sender, EventArgs e) + { + FocusMode = RangeSliderFocusMode.Default; + UpdateFocusedSliderLayout(); + } + + void HandleUpperSliderPropertyChanged(object? sender, PropertyChangedEventArgs e) + { + if (e.PropertyName == Slider.ValueProperty.PropertyName) + { + UpperValue = (StepSize == 0.0 ? upperSlider.Value : Math.Round(upperSlider.Value)) * GetUnit() + MinimumValue; + FocusMode = RangeSliderFocusMode.Upper; + UpdateInnerTrackLayout(); + } + } + + void HandlerUpperSliderFocused(object? sender, FocusEventArgs e) + { + FocusMode = RangeSliderFocusMode.Upper; + UpdateFocusedSliderLayout(); + } + + void UpdateFocusedSliderLayout() + { + double unit = GetUnit(); + lowerSlider.Minimum = upperSlider.Minimum = 0; + lowerSlider.Maximum = upperSlider.Maximum = (MaximumValue - MinimumValue) / unit; + lowerSlider.Maximum = upperSlider.Minimum = FocusMode switch + { + RangeSliderFocusMode.Lower => upperSlider.Value, + RangeSliderFocusMode.Upper => lowerSlider.Value, + _ => (lowerSlider.Value + upperSlider.Value) / 2, + }; + + if (upperSlider.Maximum - lowerSlider.Minimum != 0) + { + lowerSlider.WidthRequest = (Width - PlatformThumbSize) * (lowerSlider.Maximum - lowerSlider.Minimum) / (upperSlider.Maximum - lowerSlider.Minimum) + PlatformThumbSize; + upperSlider.WidthRequest = (Width - PlatformThumbSize) * (upperSlider.Maximum - upperSlider.Minimum) / (upperSlider.Maximum - lowerSlider.Minimum) + PlatformThumbSize; + } + } + + double GetUnit() + { + double unit = StepSize == 0 ? 1 : Math.Abs(StepSize); + return MinimumValue <= MaximumValue ? unit : -unit; + } + + void UpdateSliderRanges() + { + double unit = GetUnit(); + lowerSlider.Minimum = 0; + lowerSlider.Maximum = (MaximumValue - MinimumValue) / unit; + upperSlider.Minimum = 0; + upperSlider.Maximum = (MaximumValue - MinimumValue) / unit; + } + + void UpdateLowerSliderValue() + { + lowerSlider.Value = (LowerValue - MinimumValue) / GetUnit(); + } + + void UpdateUpperSliderValue() + { + upperSlider.Value = (UpperValue - MinimumValue) / GetUnit(); + } + + void UpdateOuterTrackLayout() + { + outerTrack.TranslationX = PlatformThumbSize / 2 - OuterTrackSize / 2; + outerTrack.WidthRequest = (Width - PlatformThumbSize) + OuterTrackSize; + } + + void UpdateInnerTrackLayout() + { + innerTrack.TranslationX = (Width - PlatformThumbSize) * (lowerSlider.Value - lowerSlider.Minimum) / (upperSlider.Maximum - lowerSlider.Minimum) + PlatformThumbSize / 2 - InnerTrackSize / 2; + innerTrack.WidthRequest = (Width - PlatformThumbSize) * (upperSlider.Value - lowerSlider.Value) / (upperSlider.Maximum - lowerSlider.Minimum) + InnerTrackSize; + } + + /// Gets or sets the minimum value + public double MinimumValue + { + get => (double)GetValue(MinimumValueProperty); + set => SetValue(MinimumValueProperty, value); + } + + /// Gets or sets the maximum value + public double MaximumValue + { + get => (double)GetValue(MaximumValueProperty); + set => SetValue(MaximumValueProperty, value); + } + + /// Gets or sets the lower value + public double LowerValue + { + get => (double)GetValue(LowerValueProperty); + set => SetValue(LowerValueProperty, value); + } + + /// Gets or sets the upper value + public double UpperValue + { + get => (double)GetValue(UpperValueProperty); + set => SetValue(UpperValueProperty, value); + } + + /// Gets or sets the step size + public double StepSize + { + get => (double)GetValue(StepSizeProperty); + set => SetValue(StepSizeProperty, value); + } + + /// Gets or sets the lower thumb color + public Color LowerThumbColor + { + get => (Color)GetValue(LowerThumbColorProperty); + set => SetValue(LowerThumbColorProperty, value); + } + + /// Gets or sets the upper thumb color + public Color UpperThumbColor + { + get => (Color)GetValue(UpperThumbColorProperty); + set => SetValue(UpperThumbColorProperty, value); + } + + /// Gets or sets the inner track color + public Color InnerTrackColor + { + get => (Color)GetValue(InnerTrackColorProperty); + set => SetValue(InnerTrackColorProperty, value); + } + + /// Gets or sets the inner track size + public double InnerTrackSize + { + get => (double)GetValue(InnerTrackSizeProperty); + set => SetValue(InnerTrackSizeProperty, value); + } + + /// Gets or sets the inner track corner radius + public CornerRadius InnerTrackCornerRadius + { + get => (CornerRadius)GetValue(InnerTrackCornerRadiusProperty); + set => SetValue(InnerTrackCornerRadiusProperty, value); + } + /// Gets or sets the outer track color + public Color OuterTrackColor + { + get => (Color)GetValue(OuterTrackColorProperty); + set => SetValue(OuterTrackColorProperty, value); + } + + /// Gets or sets the outer track size + public double OuterTrackSize + { + get => (double)GetValue(OuterTrackSizeProperty); + set => SetValue(OuterTrackSizeProperty, value); + } + + /// Gets or sets the outer track corner radius + public CornerRadius OuterTrackCornerRadius + { + get => (CornerRadius)GetValue(OuterTrackCornerRadiusProperty); + set => SetValue(OuterTrackCornerRadiusProperty, value); + } + + /// Gets the focus mode + public RangeSliderFocusMode FocusMode + { + get => (RangeSliderFocusMode)GetValue(FocusModeProperty); + private set => SetValue(FocusModeProperty, value); + } + + /// Gets the platform-specific thumb size +#if WINDOWS + public const double PlatformThumbSize = 17.5; +#else + public const double PlatformThumbSize = 31.5; +#endif +} From dd9ed673b151e05312775175e1d060ed6a1d6648 Mon Sep 17 00:00:00 2001 From: Stephen Quan Date: Tue, 2 Dec 2025 22:02:03 +1100 Subject: [PATCH 02/19] Implemented various Copilot review suggestions --- .../Views/RangeSlider/RangeSliderPage.xaml | 2 +- .../Views/RangeSlider/RangeSliderPage.xaml.cs | 4 +- .../Defaults/RangeSliderDefaults.shared.cs | 2 +- .../Views/RangeSlider/RangeSlider.shared.cs | 56 ++++++++++--------- 4 files changed, 33 insertions(+), 31 deletions(-) diff --git a/samples/CommunityToolkit.Maui.Sample/Pages/Views/RangeSlider/RangeSliderPage.xaml b/samples/CommunityToolkit.Maui.Sample/Pages/Views/RangeSlider/RangeSliderPage.xaml index 2557211a7e..41dfe0190a 100644 --- a/samples/CommunityToolkit.Maui.Sample/Pages/Views/RangeSlider/RangeSliderPage.xaml +++ b/samples/CommunityToolkit.Maui.Sample/Pages/Views/RangeSlider/RangeSliderPage.xaml @@ -89,7 +89,7 @@