diff --git a/components/RadialGauge/OpenSolution.bat b/components/RadialGauge/OpenSolution.bat new file mode 100644 index 00000000..814a56d4 --- /dev/null +++ b/components/RadialGauge/OpenSolution.bat @@ -0,0 +1,3 @@ +@ECHO OFF + +powershell ..\..\tooling\ProjectHeads\GenerateSingleSampleHeads.ps1 -componentPath %CD% %* \ No newline at end of file diff --git a/components/RadialGauge/samples/Dependencies.props b/components/RadialGauge/samples/Dependencies.props new file mode 100644 index 00000000..e622e1df --- /dev/null +++ b/components/RadialGauge/samples/Dependencies.props @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/components/RadialGauge/samples/RadialGauge.Samples.csproj b/components/RadialGauge/samples/RadialGauge.Samples.csproj new file mode 100644 index 00000000..7ee8a5b1 --- /dev/null +++ b/components/RadialGauge/samples/RadialGauge.Samples.csproj @@ -0,0 +1,8 @@ + + + RadialGauge + + + + + diff --git a/components/RadialGauge/samples/RadialGauge.md b/components/RadialGauge/samples/RadialGauge.md new file mode 100644 index 00000000..c3babcbe --- /dev/null +++ b/components/RadialGauge/samples/RadialGauge.md @@ -0,0 +1,58 @@ +--- +title: RadialGauge +author: xamlbrewer +description: The Radial Gauge Control displays a value in a certain range using a needle on a circular face. +keywords: RadialGauge, Control, Input +dev_langs: + - csharp +category: Controls +subcategory: Input +discussion-id: 0 +issue-id: 0 +--- + +# RadialGauge + +The [Radial Gauge](/dotnet/api/microsoft.toolkit.uwp.ui.controls.radialgauge) control displays a value in a certain range using a needle on a circular face. This control will make data visualizations and dashboards more engaging with rich style and interactivity. +The round gauges are powerful, easy to use, and highly configurable to present dashboards capable of displaying clocks, industrial panels, automotive dashboards, and even aircraft cockpits. + +The Radial Gauge supports animated transitions between configuration states. The control gradually animates as it redraws changes to the needle, needle position, scale range, color range, and more. + +> [!Sample RadialGaugeSample] + +## Properties + +| Property | Type | Description | +| -- | -- | -- | +| Column | double | Gets or sets the column of the scale | +| IsInteractive | bool | Gets or sets a value indicating whether the control accepts setting its value through interaction | +| MaxAngle | int | Gets or sets the end angle of the scale, which corresponds with the Maximum value, in degrees | +| Maximum | double | Gets or sets the maximum value of the scale | +| MinAngle | int | Gets or sets the start angle of the scale, which corresponds with the Minimum value, in degrees | +| Minimum | double | Gets or sets the minimum value of the scale | +| NeedleBrush | SolidColorBrush | Gets or sets the needle background | +| NeedleBorderBrush | SolidColorBrush | Gets or sets the needle border | +| NeedleBorderThickness | double | Gets or sets the thickness of the border | +| NeedleLength | double | Gets or sets the needle length, in percentage of the gauge radius | +| NeedleWidth | double | Gets or sets the needle width, in percentage of the gauge radius | +| NormalizedMaxAngle | double | Gets the normalized maximum angle | +| NormalizedMinAngle | double | Gets the normalized minimum angle | +| ScaleBrush | Brush | Gets or sets the scale brush | +| ScalePadding | double | Gets or sets the distance of the scale from the outside of the control, in percentage of the gauge radius | +| ScaleTickCornerRadius | double | Gets or sets the cornerradius of the scale tick +| ScaleTickBrush | SolidColorBrush | Gets or sets the scale tick brush | +| ScaleTickLength | double | Gets or sets the length of the scaleticks, in percentage of the gauge radius | +| ScaleTickWidth | double | Gets or sets the width of the scale ticks, in percentage of the gauge radius | +| ScaleWidth | double | Gets or sets the width of the scale, in percentage of the gauge radius | +| StepSize | double | Gets or sets the rounding interval for the Value | +| TickBrush | SolidColorBrush | Gets or sets the outer tick brush | +| TickCornerRadius | double | Gets or sets the cornerradius of the tick | +| TickLength | double | Gets or sets the length of the ticks, in percentage of the gauge radius | +| TickPadding | double | Gets or sets the distance of the ticks from the outside of the control, in percentage of the gauge radius | +| TickSpacing | int | Gets or sets the tick spacing, in units | +| TickWidth | double | Gets or sets the width of the ticks, in percentage of the gauge radius | +| TrailBrush | Brush | Gets or sets the trail brush | +| Unit | string | Gets or sets the displayed unit measure | +| Value | double | Gets or sets the current value | +| ValueAngle | double | Gets or sets the current angle of the needle (between MinAngle and MaxAngle). Setting the angle will update the Value | +| ValueStringFormat | string | Gets or sets the value string format | diff --git a/components/RadialGauge/samples/RadialGaugeSample.xaml b/components/RadialGauge/samples/RadialGaugeSample.xaml new file mode 100644 index 00000000..579bbeca --- /dev/null +++ b/components/RadialGauge/samples/RadialGaugeSample.xaml @@ -0,0 +1,35 @@ + + + + + + + diff --git a/components/RadialGauge/samples/RadialGaugeSample.xaml.cs b/components/RadialGauge/samples/RadialGaugeSample.xaml.cs new file mode 100644 index 00000000..d73d2def --- /dev/null +++ b/components/RadialGauge/samples/RadialGaugeSample.xaml.cs @@ -0,0 +1,35 @@ +// 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 RadialGaugeExperiment.Samples; + +/// +/// An example sample page of a custom control inheriting from Panel. +/// +[ToolkitSampleBoolOption("Enabled", true, Title = "IsEnabled")] +[ToolkitSampleNumericOption("Value", 120, 0, 240, 1, false, Title = "Value")] +[ToolkitSampleNumericOption("StepSize", 30, 5, 30, 1, false, Title = "StepSize")] +[ToolkitSampleBoolOption("IsInteractive", true, Title = "IsInteractive")] +[ToolkitSampleNumericOption("TickSpacing", 15, 10, 30, 1, false, Title = "TickSpacing")] +[ToolkitSampleNumericOption("ScaleWidth", 12, 4, 50, 1, false, Title = "ScaleWidth")] +[ToolkitSampleNumericOption("MinAngle", -150, -150, 360, 1, false, Title = "MinAngle")] +[ToolkitSampleNumericOption("MaxAngle", 150, 0, 360, 1, false, Title = "MaxAngle")] +[ToolkitSampleNumericOption("NeedleWidth", 4, 0, 10, 1, false, Title = "NeedleWidth")] +[ToolkitSampleNumericOption("NeedleLength", 60, 0, 100, 1, false, Title = "NeedleLength")] +[ToolkitSampleNumericOption("TickLength", 6, 0, 30, 1, false, Title = "TickLength")] +[ToolkitSampleNumericOption("TickWidth", 2, 0, 30, 1, false, Title = "TickWidth")] +[ToolkitSampleNumericOption("ScalePadding", 0, 0, 100, 1, false, Title = "ScalePadding")] +[ToolkitSampleNumericOption("TickPadding", 24, 0, 100, 1, false, Title = "TickPadding")] +[ToolkitSampleNumericOption("ScaleTickWidth", 0, 0, 20, 1, false, Title = "ScaleTickWidth")] + +[ToolkitSample(id: nameof(RadialGaugeSample), "RadialGauge", description: $"A sample for showing how to create and use a {nameof(RadialGauge)} control.")] +public sealed partial class RadialGaugeSample : Page +{ + public RadialGaugeSample() + { + this.InitializeComponent(); + } +} diff --git a/components/RadialGauge/src/AdditionalAssemblyInfo.cs b/components/RadialGauge/src/AdditionalAssemblyInfo.cs new file mode 100644 index 00000000..343502a1 --- /dev/null +++ b/components/RadialGauge/src/AdditionalAssemblyInfo.cs @@ -0,0 +1,13 @@ +// 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 System.Runtime.CompilerServices; + +// These `InternalsVisibleTo` calls are intended to make it easier for +// for any internal code to be testable in all the different test projects +// used with the Labs infrastructure. +[assembly: InternalsVisibleTo("RadialGauge.Tests.Uwp")] +[assembly: InternalsVisibleTo("RadialGauge.Tests.WinAppSdk")] +[assembly: InternalsVisibleTo("CommunityToolkit.Tests.Uwp")] +[assembly: InternalsVisibleTo("CommunityToolkit.Tests.WinAppSdk")] diff --git a/components/RadialGauge/src/CommunityToolkit.WinUI.Controls.RadialGauge.csproj b/components/RadialGauge/src/CommunityToolkit.WinUI.Controls.RadialGauge.csproj new file mode 100644 index 00000000..c2745c5c --- /dev/null +++ b/components/RadialGauge/src/CommunityToolkit.WinUI.Controls.RadialGauge.csproj @@ -0,0 +1,22 @@ + + + RadialGauge + This package contains RadialGauge. + 0.0.1 + + + CommunityToolkit.WinUI.Controls.RadialGaugeRns + + + + + + + + + + + + $(PackageIdPrefix).$(PackageIdVariant).Controls.$(ToolkitComponentName) + + diff --git a/components/RadialGauge/src/Dependencies.props b/components/RadialGauge/src/Dependencies.props new file mode 100644 index 00000000..e622e1df --- /dev/null +++ b/components/RadialGauge/src/Dependencies.props @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/components/RadialGauge/src/MultiTarget.props b/components/RadialGauge/src/MultiTarget.props new file mode 100644 index 00000000..18f6c7c9 --- /dev/null +++ b/components/RadialGauge/src/MultiTarget.props @@ -0,0 +1,9 @@ + + + + uwp;wasdk; + + diff --git a/components/RadialGauge/src/RadialGauge.Input.cs b/components/RadialGauge/src/RadialGauge.Input.cs new file mode 100644 index 00000000..f7873b47 --- /dev/null +++ b/components/RadialGauge/src/RadialGauge.Input.cs @@ -0,0 +1,119 @@ +// 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 VirtualKey = Windows.System.VirtualKey; +using VirtualKeyModifiers = Windows.System.VirtualKeyModifiers; + +namespace CommunityToolkit.WinUI.Controls; +public partial class RadialGauge : RangeBase +{ + private void RadialGauge_ManipulationDelta(object sender, ManipulationDeltaRoutedEventArgs e) + { + SetGaugeValueFromPoint(e.Position); + } + + private void RadialGauge_Tapped(object sender, TappedRoutedEventArgs e) + { + SetGaugeValueFromPoint(e.GetPosition(this)); + } + + private void RadialGauge_PointerReleased(object sender, PointerRoutedEventArgs e) + { + if (IsInteractive) + { + e.Handled = true; + } + } + + private void SetKeyboardAccelerators() + { + // Small step + AddKeyboardAccelerator(VirtualKeyModifiers.None, VirtualKey.Left, static (_, kaea) => + { + if (kaea.Element is RadialGauge gauge) + { + gauge.Value = Math.Max(gauge.Minimum, gauge.Value - Math.Max(gauge.StepSize, gauge.SmallChange)); + kaea.Handled = true; + } + }); + + AddKeyboardAccelerator(VirtualKeyModifiers.None, VirtualKey.Up, static (_, kaea) => + { + if (kaea.Element is RadialGauge gauge) + { + gauge.Value = Math.Min(gauge.Maximum, gauge.Value + Math.Max(gauge.StepSize, gauge.SmallChange)); + kaea.Handled = true; + } + }); + + AddKeyboardAccelerator(VirtualKeyModifiers.None, VirtualKey.Right, static (_, kaea) => + { + if (kaea.Element is RadialGauge gauge) + { + gauge.Value = Math.Min(gauge.Maximum, gauge.Value + Math.Max(gauge.StepSize, gauge.SmallChange)); + kaea.Handled = true; + } + }); + + AddKeyboardAccelerator(VirtualKeyModifiers.None, VirtualKey.Down, static (_, kaea) => + { + if (kaea.Element is RadialGauge gauge) + { + gauge.Value = Math.Max(gauge.Minimum, gauge.Value - Math.Max(gauge.StepSize, gauge.SmallChange)); + kaea.Handled = true; + } + }); + + // Large step + AddKeyboardAccelerator(VirtualKeyModifiers.Control, VirtualKey.Left, static (_, kaea) => + { + if (kaea.Element is RadialGauge gauge) + { + gauge.Value = Math.Max(gauge.Minimum, gauge.Value - Math.Max(gauge.StepSize, gauge.LargeChange)); + kaea.Handled = true; + } + }); + + AddKeyboardAccelerator(VirtualKeyModifiers.Control, VirtualKey.Up, static (_, kaea) => + { + if (kaea.Element is RadialGauge gauge) + { + gauge.Value = Math.Min(gauge.Maximum, gauge.Value + Math.Max(gauge.StepSize, gauge.LargeChange)); + kaea.Handled = true; + } + }); + + AddKeyboardAccelerator(VirtualKeyModifiers.Control, VirtualKey.Right, static (_, kaea) => + { + if (kaea.Element is RadialGauge gauge) + { + gauge.Value = Math.Min(gauge.Maximum, gauge.Value + Math.Max(gauge.StepSize, gauge.LargeChange)); + kaea.Handled = true; + } + }); + + AddKeyboardAccelerator(VirtualKeyModifiers.Control, VirtualKey.Down, static (_, kaea) => + { + if (kaea.Element is RadialGauge gauge) + { + gauge.Value = Math.Max(gauge.Minimum, gauge.Value - Math.Max(gauge.StepSize, gauge.LargeChange)); + kaea.Handled = true; + } + }); + } + + private void AddKeyboardAccelerator( + VirtualKeyModifiers keyModifiers, + VirtualKey key, + TypedEventHandler handler) + { + var accelerator = new KeyboardAccelerator() + { + Modifiers = keyModifiers, + Key = key + }; + accelerator.Invoked += handler; + KeyboardAccelerators.Add(accelerator); + } +} diff --git a/components/RadialGauge/src/RadialGauge.Properties.cs b/components/RadialGauge/src/RadialGauge.Properties.cs new file mode 100644 index 00000000..f274cfe0 --- /dev/null +++ b/components/RadialGauge/src/RadialGauge.Properties.cs @@ -0,0 +1,421 @@ +// 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.Helpers; + +namespace CommunityToolkit.WinUI.Controls; +public partial class RadialGauge : RangeBase +{ + /// + /// Identifies the property. + /// + public static readonly DependencyProperty IsInteractiveProperty = + DependencyProperty.Register(nameof(IsInteractive), typeof(bool), typeof(RadialGauge), new PropertyMetadata(true, OnInteractivityChanged)); + + /// + /// Identifies the ScaleWidth dependency property. + /// + public static readonly DependencyProperty ScaleWidthProperty = + DependencyProperty.Register(nameof(ScaleWidth), typeof(double), typeof(RadialGauge), new PropertyMetadata(12.0, OnScaleChanged)); + + /// + /// Identifies the optional StepSize property. + /// + public static readonly DependencyProperty StepSizeProperty = + DependencyProperty.Register(nameof(StepSize), typeof(double), typeof(RadialGauge), new PropertyMetadata(0.0)); + + /// + /// Identifies the NeedleBrush dependency property. + /// + public static readonly DependencyProperty NeedleBrushProperty = + DependencyProperty.Register(nameof(NeedleBrush), typeof(SolidColorBrush), typeof(RadialGauge), new PropertyMetadata(null, OnFaceChanged)); + + /// + /// Identifies the NeedleBrush dependency property. + /// + public static readonly DependencyProperty NeedleBorderBrushProperty = + DependencyProperty.Register(nameof(NeedleBorderBrush), typeof(SolidColorBrush), typeof(RadialGauge), new PropertyMetadata(null, OnFaceChanged)); + + /// + /// Identifies the Unit dependency property. + /// + public static readonly DependencyProperty UnitProperty = + DependencyProperty.Register(nameof(Unit), typeof(string), typeof(RadialGauge), new PropertyMetadata(string.Empty, OnUnitChanged)); + + /// + /// Identifies the TrailBrush dependency property. + /// + public static readonly DependencyProperty TrailBrushProperty = + DependencyProperty.Register(nameof(TrailBrush), typeof(Brush), typeof(RadialGauge), new PropertyMetadata(null)); + + /// + /// Identifies the ScaleBrush dependency property. + /// + public static readonly DependencyProperty ScaleBrushProperty = + DependencyProperty.Register(nameof(ScaleBrush), typeof(Brush), typeof(RadialGauge), new PropertyMetadata(null)); + + /// + /// Identifies the ScaleTickBrush dependency property. + /// + public static readonly DependencyProperty ScaleTickBrushProperty = + DependencyProperty.Register(nameof(ScaleTickBrush), typeof(Brush), typeof(RadialGauge), new PropertyMetadata(null, OnFaceChanged)); + + /// + /// Identifies the TickBrush dependency property. + /// + public static readonly DependencyProperty TickBrushProperty = + DependencyProperty.Register(nameof(TickBrush), typeof(SolidColorBrush), typeof(RadialGauge), new PropertyMetadata(null, OnFaceChanged)); + + /// + /// Identifies the ValueStringFormat dependency property. + /// + public static readonly DependencyProperty ValueStringFormatProperty = + DependencyProperty.Register(nameof(ValueStringFormat), typeof(string), typeof(RadialGauge), new PropertyMetadata("N0", (s, e) => OnValueChanged(s))); + + /// + /// Identifies the NeedleLength dependency property. + /// + public static readonly DependencyProperty NeedleLengthProperty = + DependencyProperty.Register(nameof(NeedleLength), typeof(double), typeof(RadialGauge), new PropertyMetadata(58d, OnFaceChanged)); + + /// + /// Identifies the NeedleLength dependency property. + /// + public static readonly DependencyProperty NeedleBorderThicknessProperty = + DependencyProperty.Register(nameof(NeedleBorderThickness), typeof(double), typeof(RadialGauge), new PropertyMetadata(1d, OnFaceChanged)); + + /// + /// Identifies the NeedleWidth dependency property. + /// + public static readonly DependencyProperty NeedleWidthProperty = + DependencyProperty.Register(nameof(NeedleWidth), typeof(double), typeof(RadialGauge), new PropertyMetadata(5d, OnFaceChanged)); + + /// + /// Identifies the ScalePadding dependency property. + /// + public static readonly DependencyProperty ScalePaddingProperty = + DependencyProperty.Register(nameof(ScalePadding), typeof(double), typeof(RadialGauge), new PropertyMetadata(0d, OnFaceChanged)); + + /// + /// Identifies the ScaleTickWidth dependency property. + /// + public static readonly DependencyProperty ScaleTickWidthProperty = + DependencyProperty.Register(nameof(ScaleTickWidth), typeof(double), typeof(RadialGauge), new PropertyMetadata(0d, OnFaceChanged)); + + /// + /// Identifies the ScaleTickWidth dependency property. + /// + public static readonly DependencyProperty ScaleTickLengthProperty = + DependencyProperty.Register(nameof(ScaleTickLength), typeof(double), typeof(RadialGauge), new PropertyMetadata(12d, OnFaceChanged)); + + + /// + /// Identifies the ScaleTickWidth dependency property. + /// + public static readonly DependencyProperty ScaleTickCornerRadiusProperty = + DependencyProperty.Register(nameof(ScaleTickCornerRadius), typeof(double), typeof(RadialGauge), new PropertyMetadata(2d, OnFaceChanged)); + + /// + /// Identifies the TickSpacing dependency property. + /// + public static readonly DependencyProperty TickSpacingProperty = + DependencyProperty.Register(nameof(TickSpacing), typeof(int), typeof(RadialGauge), new PropertyMetadata(15, OnFaceChanged)); + + /// + /// Identifies the TickWidth dependency property. + /// + public static readonly DependencyProperty TickWidthProperty = + DependencyProperty.Register(nameof(TickWidth), typeof(double), typeof(RadialGauge), new PropertyMetadata(2d, OnFaceChanged)); + + /// + /// Identifies the TickLength dependency property. + /// + public static readonly DependencyProperty TickLengthProperty = + DependencyProperty.Register(nameof(TickLength), typeof(double), typeof(RadialGauge), new PropertyMetadata(6d, OnFaceChanged)); + + /// + /// Identifies the TickPadding dependency property. + /// + public static readonly DependencyProperty TickPaddingProperty = + DependencyProperty.Register(nameof(TickPadding), typeof(double), typeof(RadialGauge), new PropertyMetadata(24d, OnFaceChanged)); + + /// + /// Identifies the TickCornerRadius dependency property. + /// + public static readonly DependencyProperty TickCornerRadiusProperty = + DependencyProperty.Register(nameof(TickCornerRadius), typeof(double), typeof(RadialGauge), new PropertyMetadata(2d, OnFaceChanged)); + + /// + /// Identifies the MinAngle dependency property. + /// + public static readonly DependencyProperty MinAngleProperty = + DependencyProperty.Register(nameof(MinAngle), typeof(int), typeof(RadialGauge), new PropertyMetadata(-150, OnScaleChanged)); + + /// + /// Identifies the MaxAngle dependency property. + /// + public static readonly DependencyProperty MaxAngleProperty = + DependencyProperty.Register(nameof(MaxAngle), typeof(int), typeof(RadialGauge), new PropertyMetadata(150, OnScaleChanged)); + + /// + /// Identifies the ValueAngle dependency property. + /// + protected static readonly DependencyProperty ValueAngleProperty = + DependencyProperty.Register(nameof(ValueAngle), typeof(double), typeof(RadialGauge), new PropertyMetadata(null)); + + /// + /// Gets or sets the rounding interval for the Value. + /// + public double StepSize + { + get { return (double)GetValue(StepSizeProperty); } + set { SetValue(StepSizeProperty, value); } + } + + /// + /// Gets or sets a value indicating whether the control accepts setting its value through interaction. + /// + public bool IsInteractive + { + get { return (bool)GetValue(IsInteractiveProperty); } + set { SetValue(IsInteractiveProperty, value); } + } + + /// + /// Gets or sets the width of the scale, in percentage of the gauge radius. + /// + public double ScaleWidth + { + get { return (double)GetValue(ScaleWidthProperty); } + set { SetValue(ScaleWidthProperty, value); } + } + + /// + /// Gets or sets the displayed unit measure. + /// + public string Unit + { + get { return (string)GetValue(UnitProperty); } + set { SetValue(UnitProperty, value); } + } + + /// + /// Gets or sets the needle brush. + /// + public SolidColorBrush NeedleBrush + { + get { return (SolidColorBrush)GetValue(NeedleBrushProperty); } + set { SetValue(NeedleBrushProperty, value); } + } + + /// + /// Gets or sets the needle border brush. + /// + public SolidColorBrush NeedleBorderBrush + { + get { return (SolidColorBrush)GetValue(NeedleBorderBrushProperty); } + set { SetValue(NeedleBorderBrushProperty, value); } + } + + /// + /// Gets or sets the trail brush. + /// + public Brush TrailBrush + { + get { return (Brush)GetValue(TrailBrushProperty); } + set { SetValue(TrailBrushProperty, value); } + } + + /// + /// Gets or sets the scale brush. + /// + public Brush ScaleBrush + { + get { return (Brush)GetValue(ScaleBrushProperty); } + set { SetValue(ScaleBrushProperty, value); } + } + + /// + /// Gets or sets the scale tick brush. + /// + public SolidColorBrush ScaleTickBrush + { + get { return (SolidColorBrush)GetValue(ScaleTickBrushProperty); } + set { SetValue(ScaleTickBrushProperty, value); } + } + + /// + /// Gets or sets the scale tick cornerradius. + /// + public double ScaleTickCornerRadius + { + get { return (double)GetValue(ScaleTickCornerRadiusProperty); } + set { SetValue(ScaleTickCornerRadiusProperty, value); } + } + + + /// + /// Gets or sets the outer tick brush. + /// + public SolidColorBrush TickBrush + { + get { return (SolidColorBrush)GetValue(TickBrushProperty); } + set { SetValue(TickBrushProperty, value); } + } + + /// + /// Gets or sets the value string format. + /// + public string ValueStringFormat + { + get { return (string)GetValue(ValueStringFormatProperty); } + set { SetValue(ValueStringFormatProperty, value); } + } + + /// + /// Gets or sets the tick spacing, in units. Values of zero or less will be ignored when drawing. + /// + public int TickSpacing + { + get { return (int)GetValue(TickSpacingProperty); } + set { SetValue(TickSpacingProperty, value); } + } + + /// + /// Gets or sets the needle length, in percentage of the gauge radius. + /// + public double NeedleLength + { + get { return (double)GetValue(NeedleLengthProperty); } + set { SetValue(NeedleLengthProperty, value); } + } + + + /// + /// Gets or sets the needle length, in percentage of the gauge radius. + /// + public double NeedleBorderThickness + { + get { return (double)GetValue(NeedleBorderThicknessProperty); } + set { SetValue(NeedleBorderThicknessProperty, value); } + } + + /// + /// Gets or sets the needle width, in percentage of the gauge radius. + /// + public double NeedleWidth + { + get { return (double)GetValue(NeedleWidthProperty); } + set { SetValue(NeedleWidthProperty, value); } + } + + /// + /// Gets or sets the distance of the scale from the outside of the control, in percentage of the gauge radius. + /// + public double ScalePadding + { + get { return (double)GetValue(ScalePaddingProperty); } + set { SetValue(ScalePaddingProperty, value); } + } + + /// + /// Gets or sets the distance of the ticks from the outside of the control, in percentage of the gauge radius. + /// + public double TickPadding + { + get { return (double)GetValue(TickPaddingProperty); } + set { SetValue(TickPaddingProperty, value); } + } + + /// + /// Gets or sets the width of the scale ticks, in percentage of the gauge radius. + /// + public double ScaleTickWidth + { + get { return (double)GetValue(ScaleTickWidthProperty); } + set { SetValue(ScaleTickWidthProperty, value); } + } + + /// + /// Gets or sets the length of the ticks, in percentage of the gauge radius. + /// + public double ScaleTickLength + { + get { return (double)GetValue(ScaleTickLengthProperty); } + set { SetValue(ScaleTickLengthProperty, value); } + } + + /// + /// Gets or sets the length of the ticks, in percentage of the gauge radius. + /// + public double TickLength + { + get { return (double)GetValue(TickLengthProperty); } + set { SetValue(TickLengthProperty, value); } + } + + /// + /// Gets or sets the width of the ticks, in percentage of the gauge radius. + /// + public double TickWidth + { + get { return (double)GetValue(TickWidthProperty); } + set { SetValue(TickWidthProperty, value); } + } + + /// + /// Gets or sets the CornerRadius of the ticks, in percentage of the gauge radius. + /// + public double TickCornerRadius + { + get { return (double)GetValue(TickCornerRadiusProperty); } + set { SetValue(TickCornerRadiusProperty, value); } + } + + /// + /// Gets or sets the start angle of the scale, which corresponds with the Minimum value, in degrees. + /// + /// Changing MinAngle may require retemplating the control. + public int MinAngle + { + get { return (int)GetValue(MinAngleProperty); } + set { SetValue(MinAngleProperty, value); } + } + + /// + /// Gets or sets the end angle of the scale, which corresponds with the Maximum value, in degrees. + /// + /// Changing MaxAngle may require retemplating the control. + public int MaxAngle + { + get { return (int)GetValue(MaxAngleProperty); } + set { SetValue(MaxAngleProperty, value); } + } + + /// + /// Gets or sets the current angle of the needle (between MinAngle and MaxAngle). Setting the angle will update the Value. + /// + protected double ValueAngle + { + get { return (double)GetValue(ValueAngleProperty); } + set { SetValue(ValueAngleProperty, value); } + } + + private static void OnUnitChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + OnUnitChanged(d); + } + private static void OnScaleChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + OnScaleChanged(d); + } + + private static void OnFaceChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + if (!DesignTimeHelpers.IsRunningInLegacyDesignerMode) + { + OnFaceChanged(d); + } + } +} diff --git a/components/RadialGauge/src/RadialGauge.cs b/components/RadialGauge/src/RadialGauge.cs new file mode 100644 index 00000000..abb84889 --- /dev/null +++ b/components/RadialGauge/src/RadialGauge.cs @@ -0,0 +1,572 @@ +// 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.Helpers; +using System.Numerics; +#if WINAPPSDK +using Path = Microsoft.UI.Xaml.Shapes.Path; +using Microsoft.UI.Xaml.Hosting; +using Microsoft.UI.Composition; +#else +using Path = Windows.UI.Xaml.Shapes.Path; +using Windows.UI.Xaml.Hosting; +using Windows.UI.Composition; +#endif + +namespace CommunityToolkit.WinUI.Controls; +/// +/// A Modern UI Radial Gauge using XAML and Composition API. +/// The scale of the gauge is a clockwise arc that sweeps from MinAngle (default lower left, at -150°) to MaxAngle (default lower right, at +150°). +/// +//// All calculations are for a 200x200 square. The viewbox will do the rest. +[TemplatePart(Name = ContainerPartName, Type = typeof(Grid))] +[TemplatePart(Name = ScalePartName, Type = typeof(Path))] +[TemplatePart(Name = TrailPartName, Type = typeof(Path))] +[TemplatePart(Name = ValueTextPartName, Type = typeof(TextBlock))] +[TemplateVisualState(Name = NormalState, GroupName = CommonStates)] +[TemplateVisualState(Name = DisabledState, GroupName = CommonStates)] +public partial class RadialGauge : RangeBase +{ + // States + private const string NormalState = "Normal"; + private const string DisabledState = "Disabled"; + private const string CommonStates = "CommonStates"; + + // Template Parts. + private const string ContainerPartName = "PART_Container"; + private const string ScalePartName = "PART_Scale"; + private const string TrailPartName = "PART_Trail"; + private const string ValueTextPartName = "PART_ValueText"; + private const string UnitTextPartName = "PART_UnitText"; + + // For convenience. + private const double Degrees2Radians = Math.PI / 180; + + // High-contrast accessibility + private static readonly ThemeListener ThemeListener = new ThemeListener(); + private SolidColorBrush? _needleBrush; + private SolidColorBrush? _needleBorderBrush; + private Brush? _trailBrush; + private Brush? _scaleBrush; + private SolidColorBrush? _scaleTickBrush; + private SolidColorBrush? _tickBrush; + private Brush? _foreground; + + private double _normalizedMinAngle; + private double _normalizedMaxAngle; + + private Compositor? _compositor; + private ContainerVisual? _root; + private CompositionSpriteShape? _needle; + + /// + /// Initializes a new instance of the class. + /// Create a default radial gauge control. + /// + public RadialGauge() + { + DefaultStyleKey = typeof(RadialGauge); + + SmallChange = 1; + LargeChange = 10; + + SetKeyboardAccelerators(); + } + + private void RadialGauge_Unloaded(object sender, RoutedEventArgs e) + { + // TODO: We should just use a WeakEventListener for ThemeChanged here, but ours currently doesn't support it. + // See proposal for general helper here: https://github.com/CommunityToolkit/dotnet/issues/404 + ThemeListener.ThemeChanged -= ThemeListener_ThemeChanged; + PointerReleased -= RadialGauge_PointerReleased; + IsEnabledChanged -= RadialGauge_IsEnabledChanged; + Unloaded -= RadialGauge_Unloaded; + } + + /// + /// Update the visual state of the control when its template is changed. + /// + protected override void OnApplyTemplate() + { + PointerReleased -= RadialGauge_PointerReleased; + ThemeListener.ThemeChanged -= ThemeListener_ThemeChanged; + IsEnabledChanged -= RadialGauge_IsEnabledChanged; + Unloaded -= RadialGauge_Unloaded; + + // Remember local brushes. + _needleBrush = ReadLocalValue(NeedleBrushProperty) as SolidColorBrush; + _needleBorderBrush = ReadLocalValue(NeedleBorderBrushProperty) as SolidColorBrush; + _trailBrush = ReadLocalValue(TrailBrushProperty) as SolidColorBrush; + _scaleBrush = ReadLocalValue(ScaleBrushProperty) as SolidColorBrush; + _scaleTickBrush = ReadLocalValue(ScaleTickBrushProperty) as SolidColorBrush; + _tickBrush = ReadLocalValue(TickBrushProperty) as SolidColorBrush; + _foreground = ReadLocalValue(ForegroundProperty) as SolidColorBrush; + + PointerReleased += RadialGauge_PointerReleased; + ThemeListener.ThemeChanged += ThemeListener_ThemeChanged; + IsEnabledChanged += RadialGauge_IsEnabledChanged; + Unloaded += RadialGauge_Unloaded; + + // Apply color scheme. + OnColorsChanged(); + OnUnitChanged(this); + OnEnabledChanged(); + OnInteractivityChanged(this); + base.OnApplyTemplate(); + } + + private void ThemeListener_ThemeChanged(ThemeListener sender) + { + OnColorsChanged(); + } + + private void RadialGauge_IsEnabledChanged(object sender, DependencyPropertyChangedEventArgs e) + { + OnEnabledChanged(); + } + + /// + /// Gets the normalized minimum angle. + /// + /// The minimum angle in the range from -180 to 180. + protected double NormalizedMinAngle => _normalizedMinAngle; + + /// + /// Gets the normalized maximum angle. + /// + /// The maximum angle, in the range from -180 to 540. + protected double NormalizedMaxAngle => _normalizedMaxAngle; + + /// + protected override AutomationPeer OnCreateAutomationPeer() + { + return new RadialGaugeAutomationPeer(this); + } + + /// + protected override void OnMinimumChanged(double oldMinimum, double newMinimum) + { + base.OnMinimumChanged(oldMinimum, newMinimum); + OnScaleChanged(this); + } + + /// + protected override void OnMaximumChanged(double oldMaximum, double newMaximum) + { + base.OnMaximumChanged(oldMaximum, newMaximum); + OnScaleChanged(this); + } + + /// + protected override void OnValueChanged(double oldValue, double newValue) + { + OnValueChanged(this); + base.OnValueChanged(oldValue, newValue); + if (AutomationPeer.ListenerExists(AutomationEvents.LiveRegionChanged)) + { + var peer = FrameworkElementAutomationPeer.FromElement(this) as RadialGaugeAutomationPeer; + peer?.RaiseValueChangedEvent(oldValue, newValue); + } + } + + private static void OnValueChanged(DependencyObject d) + { + RadialGauge radialGauge = (RadialGauge)d; + if (!double.IsNaN(radialGauge.Value)) + { + if (radialGauge.StepSize != 0) + { + radialGauge.Value = radialGauge.RoundToMultiple(radialGauge.Value, radialGauge.StepSize); + } + + var middleOfScale = 100 - radialGauge.ScalePadding - (radialGauge.ScaleWidth / 2); + if (middleOfScale >= 0) + { + var valueText = radialGauge.GetTemplateChild(ValueTextPartName) as TextBlock; + radialGauge.ValueAngle = radialGauge.ValueToAngle(radialGauge.Value); + + // Needle + if (radialGauge._needle != null) + { + radialGauge._needle.RotationAngleInDegrees = (float)radialGauge.ValueAngle; + } + + // Trail + var trail = radialGauge.GetTemplateChild(TrailPartName) as Path; + if (trail != null) + { + if (radialGauge.ValueAngle > radialGauge.NormalizedMinAngle) + { + trail.Visibility = Visibility.Visible; + + if (radialGauge.ValueAngle - radialGauge.NormalizedMinAngle == 360) + { + // Draw full circle. + var eg = new EllipseGeometry + { + Center = new Point(100, 100), + RadiusX = 100 - radialGauge.ScalePadding - (radialGauge.ScaleWidth / 2) + }; + eg.RadiusY = eg.RadiusX; + trail.Data = eg; + } + else + { + // Draw arc. + var pg = new PathGeometry(); + var pf = new PathFigure + { + IsClosed = false, + StartPoint = radialGauge.ScalePoint(radialGauge.NormalizedMinAngle, middleOfScale) + }; + var seg = new ArcSegment + { + SweepDirection = SweepDirection.Clockwise, + IsLargeArc = radialGauge.ValueAngle > (180 + radialGauge.NormalizedMinAngle), + Size = new Size(middleOfScale, middleOfScale), + Point = radialGauge.ScalePoint(Math.Min(radialGauge.ValueAngle, radialGauge.NormalizedMaxAngle), middleOfScale) // On overflow, stop trail at MaxAngle. + }; + pf.Segments.Add(seg); + pg.Figures.Add(pf); + trail.Data = pg; + } + } + else + { + trail.Visibility = Visibility.Collapsed; + } + + } + + // Value Text + if (valueText != null) + { + valueText.Text = radialGauge.Value.ToString(radialGauge.ValueStringFormat); + } + } + } + } + + private static void OnInteractivityChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + OnInteractivityChanged(d); + } + + private static void OnInteractivityChanged(DependencyObject d) + { + RadialGauge radialGauge = (RadialGauge)d; + + if (radialGauge.IsInteractive) + { + radialGauge.Tapped += radialGauge.RadialGauge_Tapped; + radialGauge.ManipulationDelta += radialGauge.RadialGauge_ManipulationDelta; + radialGauge.ManipulationMode = ManipulationModes.TranslateX | ManipulationModes.TranslateY; + } + else + { + radialGauge.Tapped -= radialGauge.RadialGauge_Tapped; + radialGauge.ManipulationDelta -= radialGauge.RadialGauge_ManipulationDelta; + radialGauge.ManipulationMode = ManipulationModes.None; + } + } + + private static void OnScaleChanged(DependencyObject d) + { + RadialGauge radialGauge = (RadialGauge)d; + + radialGauge.UpdateNormalizedAngles(); + + if (radialGauge.GetTemplateChild(ScalePartName) is Path scale) + { + if (radialGauge.NormalizedMaxAngle - radialGauge.NormalizedMinAngle == 360) + { + // Draw full circle. + var eg = new EllipseGeometry + { + Center = new Point(100, 100), + RadiusX = 100 - radialGauge.ScalePadding - (radialGauge.ScaleWidth / 2) + }; + eg.RadiusY = eg.RadiusX; + scale.Data = eg; + } + else + { + // Draw arc. + var pg = new PathGeometry(); + var pf = new PathFigure + { + IsClosed = false + }; + var middleOfScale = 100 - radialGauge.ScalePadding - (radialGauge.ScaleWidth / 2); + pf.StartPoint = radialGauge.ScalePoint(radialGauge.NormalizedMinAngle, middleOfScale); + var seg = new ArcSegment + { + SweepDirection = SweepDirection.Clockwise, + IsLargeArc = radialGauge.NormalizedMaxAngle > (radialGauge.NormalizedMinAngle + 180), + Size = new Size(middleOfScale, middleOfScale), + Point = radialGauge.ScalePoint(radialGauge.NormalizedMaxAngle, middleOfScale) + }; + pf.Segments.Add(seg); + pg.Figures.Add(pf); + scale.Data = pg; + } + + if (!DesignTimeHelpers.IsRunningInLegacyDesignerMode) + { + OnFaceChanged(radialGauge); + } + } + } + + private static void OnFaceChanged(DependencyObject d) + { + RadialGauge radialGauge = (RadialGauge)d; + + var container = radialGauge.GetTemplateChild(ContainerPartName) as Grid; + + if (container == null || DesignTimeHelpers.IsRunningInLegacyDesignerMode) + { + // Bad template. + return; + } + + // TO DO: Replace with _radialGauge._root = container.GetVisual(); + var hostVisual = ElementCompositionPreview.GetElementVisual(container); + var root = hostVisual.Compositor.CreateContainerVisual(); + ElementCompositionPreview.SetElementChildVisual(container, root); + radialGauge._root = root; + // + + radialGauge._root.Children.RemoveAll(); + radialGauge._compositor = radialGauge._root.Compositor; + + if (radialGauge.TickSpacing > 0) + { + // Ticks. + var tick = radialGauge._compositor.CreateShapeVisual(); + tick.Size = new Vector2((float)(radialGauge.Height), (float)(radialGauge.Width)); + tick.BorderMode = CompositionBorderMode.Soft; + tick.Opacity = (float)radialGauge.TickBrush.Opacity; + + var roundedTickRectangle = radialGauge._compositor.CreateRoundedRectangleGeometry(); + roundedTickRectangle.Size = new Vector2((float)radialGauge.TickWidth, (float)radialGauge.TickLength); + roundedTickRectangle.CornerRadius = new Vector2((float)radialGauge.TickCornerRadius, (float)radialGauge.TickCornerRadius); + + for (double i = radialGauge.Minimum; i <= radialGauge.Maximum; i += radialGauge.TickSpacing) + { + var tickSpriteShape = radialGauge._compositor.CreateSpriteShape(roundedTickRectangle); + tickSpriteShape.FillBrush = radialGauge._compositor.CreateColorBrush(radialGauge.TickBrush.Color); + tickSpriteShape.Offset = new Vector2(100 - ((float)radialGauge.TickWidth / 2), (float)radialGauge.TickPadding); + tickSpriteShape.CenterPoint = new Vector2((float)radialGauge.TickWidth / 2, 100 - (float)radialGauge.TickPadding); + tickSpriteShape.RotationAngleInDegrees = (float)radialGauge.ValueToAngle(i); + tick.Shapes.Add(tickSpriteShape); + } + + radialGauge._root.Children.InsertAtTop(tick); + + // Scale Ticks. + var scaleTick = radialGauge._compositor.CreateShapeVisual(); + scaleTick.Size = new Vector2((float)(radialGauge.Height), (float)(radialGauge.Width)); + scaleTick.BorderMode = CompositionBorderMode.Soft; + scaleTick.Opacity = (float)radialGauge.ScaleTickBrush.Opacity; + + var roundedScaleTickRectangle = radialGauge._compositor.CreateRoundedRectangleGeometry(); + roundedScaleTickRectangle.Size = new Vector2((float)radialGauge.ScaleTickWidth, (float)radialGauge.ScaleTickLength); + roundedScaleTickRectangle.CornerRadius = new Vector2((float)radialGauge.ScaleTickCornerRadius, (float)radialGauge.ScaleTickCornerRadius); + + for (double i = radialGauge.Minimum; i <= radialGauge.Maximum; i += radialGauge.TickSpacing) + { + var scaleTickSpriteShape = radialGauge._compositor.CreateSpriteShape(roundedScaleTickRectangle); + scaleTickSpriteShape.FillBrush = radialGauge._compositor.CreateColorBrush(radialGauge.ScaleTickBrush.Color); + scaleTickSpriteShape.Offset = new Vector2(100 - ((float)radialGauge.ScaleTickWidth / 2), (float)radialGauge.ScalePadding); + scaleTickSpriteShape.CenterPoint = new Vector2((float)radialGauge.ScaleTickWidth / 2, 100 - (float)radialGauge.ScalePadding); + scaleTickSpriteShape.RotationAngleInDegrees = (float)radialGauge.ValueToAngle(i); + scaleTick.Shapes.Add(scaleTickSpriteShape); + } + radialGauge._root.Children.InsertAtTop(scaleTick); + } + + // Needle. + var shapeVisual = radialGauge._compositor.CreateShapeVisual(); + shapeVisual.Size = new Vector2((float)radialGauge.Height, (float)radialGauge.Width); + shapeVisual.BorderMode = CompositionBorderMode.Soft; + shapeVisual.Opacity = (float)radialGauge.NeedleBrush.Opacity; + var roundedNeedleRectangle = radialGauge._compositor.CreateRoundedRectangleGeometry(); + roundedNeedleRectangle.Size = new Vector2((float)radialGauge.NeedleWidth, (float)radialGauge.NeedleLength); + roundedNeedleRectangle.CornerRadius = new Vector2((float)radialGauge.NeedleWidth / 2, (float)radialGauge.NeedleWidth / 2); + radialGauge._needle = radialGauge._compositor.CreateSpriteShape(roundedNeedleRectangle); + radialGauge._needle.FillBrush = radialGauge._compositor.CreateColorBrush(radialGauge.NeedleBrush.Color); + radialGauge._needle.CenterPoint = new Vector2((float)radialGauge.NeedleWidth / 2, (float)radialGauge.NeedleLength); + radialGauge._needle.Offset = new Vector2(100 - ((float)radialGauge.NeedleWidth / 2), 100 - (float)radialGauge.NeedleLength); + radialGauge._needle.StrokeThickness = (float)radialGauge.NeedleBorderThickness; + radialGauge._needle.StrokeBrush = radialGauge._compositor.CreateColorBrush(radialGauge.NeedleBorderBrush.Color); + shapeVisual.Shapes.Add(radialGauge._needle); + + radialGauge._root.Children.InsertAtTop(shapeVisual); + + OnValueChanged(radialGauge); + } + + private void OnColorsChanged() + { + if (ThemeListener.IsHighContrast) + { + // Apply High Contrast Theme. + ClearBrush(_needleBrush, NeedleBrushProperty); + ClearBrush(_needleBorderBrush, NeedleBorderBrushProperty); + ClearBrush(_trailBrush, TrailBrushProperty); + ClearBrush(_scaleBrush, ScaleBrushProperty); + ClearBrush(_scaleTickBrush, ScaleTickBrushProperty); + ClearBrush(_tickBrush, TickBrushProperty); + ClearBrush(_foreground, ForegroundProperty); + } + else + { + // Apply User Defined or Default Theme. + RestoreBrush(_needleBrush, NeedleBrushProperty); + RestoreBrush(_needleBorderBrush, NeedleBorderBrushProperty); + RestoreBrush(_trailBrush, TrailBrushProperty); + RestoreBrush(_scaleBrush, ScaleBrushProperty); + RestoreBrush(_scaleTickBrush, ScaleTickBrushProperty); + RestoreBrush(_tickBrush, TickBrushProperty); + RestoreBrush(_foreground, ForegroundProperty); + } + + OnScaleChanged(this); + } + + private void OnEnabledChanged() + { + VisualStateManager.GoToState(this, IsEnabled ? NormalState : DisabledState, true); + // OnColorsChanged(); + } + + private static void OnUnitChanged(DependencyObject d) + { + RadialGauge radialGauge = (RadialGauge)d; + if (radialGauge.GetTemplateChild(UnitTextPartName) is TextBlock unitTextBlock) + { + if (string.IsNullOrEmpty(radialGauge.Unit)) + { + unitTextBlock.Visibility = Visibility.Collapsed; + } + else + { + unitTextBlock.Visibility = Visibility.Visible; + } + } + } + + private void ClearBrush(Brush? brush, DependencyProperty prop) + { + if (brush != null) + { + ClearValue(prop); + } + } + + private void RestoreBrush(Brush? source, DependencyProperty prop) + { + if (source != null) + { + SetValue(prop, source); + } + } + + private void UpdateNormalizedAngles() + { + var result = Mod(MinAngle, 360); + + if (result >= 180) + { + result = result - 360; + } + + _normalizedMinAngle = result; + + result = Mod(MaxAngle, 360); + + if (result < 180) + { + result = result + 360; + } + + if (result > NormalizedMinAngle + 360) + { + result = result - 360; + } + + _normalizedMaxAngle = result; + } + + private void SetGaugeValueFromPoint(Point p) + { + var pt = new Point(p.X - (ActualWidth / 2), -p.Y + (ActualHeight / 2)); + + var angle = Math.Atan2(pt.X, pt.Y) / Degrees2Radians; + var divider = Mod(NormalizedMaxAngle - NormalizedMinAngle, 360); + if (divider == 0) + { + divider = 360; + } + + var value = Minimum + ((Maximum - Minimum) * Mod(angle - NormalizedMinAngle, 360) / divider); + if (value < Minimum || value > Maximum) + { + // Ignore positions outside the scale angle. + return; + } + + Value = RoundToMultiple(value, StepSize); + } + + private Point ScalePoint(double angle, double middleOfScale) + { + return new Point(100 + (Math.Sin(Degrees2Radians * angle) * middleOfScale), 100 - (Math.Cos(Degrees2Radians * angle) * middleOfScale)); + } + + private double ValueToAngle(double value) + { + // Off-scale on the left. + if (value < Minimum) + { + return MinAngle; + } + + // Off-scale on the right. + if (value > Maximum) + { + return MaxAngle; + } + + return ((value - Minimum) / (Maximum - Minimum) * (NormalizedMaxAngle - NormalizedMinAngle)) + NormalizedMinAngle; + } + + private double Mod(double number, double divider) + { + var result = number % divider; + result = result < 0 ? result + divider : result; + return result; + } + + private double RoundToMultiple(double number, double multiple) + { + double modulo = number % multiple; + if (double.IsNaN(modulo)) + { + return number; + } + + if ((multiple - modulo) <= modulo) + { + modulo = multiple - modulo; + } + else + { + modulo *= -1; + } + + return number + modulo; + } +} diff --git a/components/RadialGauge/src/RadialGauge.xaml b/components/RadialGauge/src/RadialGauge.xaml new file mode 100644 index 00000000..2b607ac8 --- /dev/null +++ b/components/RadialGauge/src/RadialGauge.xaml @@ -0,0 +1,138 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/components/RadialGauge/src/RadialGaugeAutomationPeer.cs b/components/RadialGauge/src/RadialGaugeAutomationPeer.cs new file mode 100644 index 00000000..2550a8ca --- /dev/null +++ b/components/RadialGauge/src/RadialGaugeAutomationPeer.cs @@ -0,0 +1,93 @@ +// 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. + +#if WINAPPSDK +using Microsoft.UI.Xaml.Automation.Provider; +#else +using Windows.UI.Xaml.Automation.Provider; +#endif + +namespace CommunityToolkit.WinUI.Controls; + +/// +/// Exposes to Microsoft UI Automation. +/// +public class RadialGaugeAutomationPeer : + RangeBaseAutomationPeer, + IRangeValueProvider +{ + /// + /// Initializes a new instance of the class. + /// + /// The owner element to create for. + public RadialGaugeAutomationPeer(RadialGauge owner) + : base(owner) + { + } + + /// + public new bool IsReadOnly => !((RadialGauge)Owner).IsInteractive; + + /// + public new double LargeChange => ((RadialGauge)Owner).StepSize; + + /// + public new double Maximum => ((RadialGauge)Owner).Maximum; + + /// + public new double Minimum => ((RadialGauge)Owner).Minimum; + + /// + public new double SmallChange => ((RadialGauge)Owner).StepSize; + + /// + public new double Value => ((RadialGauge)Owner).Value; + + /// + public new void SetValue(double value) + { + ((RadialGauge)Owner).Value = value; + } + + /// + protected override IList? GetChildrenCore() + { + return null; + } + + /// + protected override string GetNameCore() + { + var gauge = (RadialGauge)Owner; + return "radial gauge. " + (string.IsNullOrWhiteSpace(gauge.Unit) ? "no unit specified, " : "unit " + gauge.Unit + ", ") + Value; + } + + /// + protected override object GetPatternCore(PatternInterface patternInterface) + { + if (patternInterface == PatternInterface.RangeValue) + { + // Expose RangeValue properties. + return this; + } + + return base.GetPatternCore(patternInterface); + } + + /// + protected override AutomationControlType GetAutomationControlTypeCore() + { + return AutomationControlType.Custom; + } + + /// + /// Raises the property changed event for this AutomationPeer for the provided identifier. + /// + /// Old value + /// New value + public void RaiseValueChangedEvent(double oldValue, double newValue) + { + RaisePropertyChangedEvent(RangeValuePatternIdentifiers.ValueProperty, PropertyValue.CreateDouble(oldValue), PropertyValue.CreateDouble(newValue)); + } +} diff --git a/components/RadialGauge/src/Themes/Generic.xaml b/components/RadialGauge/src/Themes/Generic.xaml new file mode 100644 index 00000000..d47de3c6 --- /dev/null +++ b/components/RadialGauge/src/Themes/Generic.xaml @@ -0,0 +1,9 @@ + + + + + + + diff --git a/components/RadialGauge/tests/RadialGauge.Tests.projitems b/components/RadialGauge/tests/RadialGauge.Tests.projitems new file mode 100644 index 00000000..d75aaae6 --- /dev/null +++ b/components/RadialGauge/tests/RadialGauge.Tests.projitems @@ -0,0 +1,14 @@ + + + + $(MSBuildAllProjects);$(MSBuildThisFileFullPath) + true + 2D8A9068-EF5F-4569-982C-D7147398B174 + + + RadialGaugeExperiment.Tests + + + + + \ No newline at end of file diff --git a/components/RadialGauge/tests/RadialGauge.Tests.shproj b/components/RadialGauge/tests/RadialGauge.Tests.shproj new file mode 100644 index 00000000..728d1a2a --- /dev/null +++ b/components/RadialGauge/tests/RadialGauge.Tests.shproj @@ -0,0 +1,13 @@ + + + + 2D8A9068-EF5F-4569-982C-D7147398B174 + 14.0 + + + + + + + + diff --git a/components/RadialGauge/tests/Test_RadialGauge.cs b/components/RadialGauge/tests/Test_RadialGauge.cs new file mode 100644 index 00000000..a130a809 --- /dev/null +++ b/components/RadialGauge/tests/Test_RadialGauge.cs @@ -0,0 +1,34 @@ +// 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 RadialGaugeExperiment.Tests; + +[TestClass] +public class Test_RadialGauge +{ + /// + /// Verifies that the UIA name is valid and makes sense + /// + [TestCategory("Test_RadialGauge_Accesibility")] + [UITestMethod] + public void VerifyUIAName() + { + var gauge = new RadialGauge() + { + Minimum = 0, + Maximum = 100, + Value = 20 + }; + + var gaugePeer = FrameworkElementAutomationPeer.CreatePeerForElement(gauge); + + Assert.IsTrue(gaugePeer.GetName().Contains(gauge.Value.ToString()), "Verify that the UIA name contains the value of the RadialGauge."); + Assert.IsTrue(gaugePeer.GetName().Contains("no unit"), "The UIA name should indicate that unit was not specified."); + + gauge.Unit = "KM/H"; + Assert.IsTrue(gaugePeer.GetName().Contains(gauge.Unit), "The UIA name should report the unit of the RadialGauge."); + } +}