diff --git a/components/GradientSlider/OpenSolution.bat b/components/GradientSlider/OpenSolution.bat new file mode 100644 index 000000000..814a56d4b --- /dev/null +++ b/components/GradientSlider/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/GradientSlider/samples/Assets/icon.png b/components/GradientSlider/samples/Assets/icon.png new file mode 100644 index 000000000..8435bcaa9 Binary files /dev/null and b/components/GradientSlider/samples/Assets/icon.png differ diff --git a/components/GradientSlider/samples/Dependencies.props b/components/GradientSlider/samples/Dependencies.props new file mode 100644 index 000000000..e622e1df4 --- /dev/null +++ b/components/GradientSlider/samples/Dependencies.props @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/components/GradientSlider/samples/GradientSlider.Samples.csproj b/components/GradientSlider/samples/GradientSlider.Samples.csproj new file mode 100644 index 000000000..676ed9987 --- /dev/null +++ b/components/GradientSlider/samples/GradientSlider.Samples.csproj @@ -0,0 +1,15 @@ + + + + + GradientSlider + + + + + + + GradientSliderSample.xaml + + + diff --git a/components/GradientSlider/samples/GradientSlider.md b/components/GradientSlider/samples/GradientSlider.md new file mode 100644 index 000000000..197b06c43 --- /dev/null +++ b/components/GradientSlider/samples/GradientSlider.md @@ -0,0 +1,25 @@ +--- +title: GradientSlider +author: githubaccount +description: TODO: Your experiment's description here +keywords: GradientSlider, Control, Layout +dev_langs: + - csharp +category: Controls +subcategory: Layout +discussion-id: 0 +issue-id: 0 +icon: assets/icon.png +--- + + + + + + + + + +# GradientSlider + +> [!SAMPLE GradientSliderSample] diff --git a/components/GradientSlider/samples/GradientSliderSample.xaml b/components/GradientSlider/samples/GradientSliderSample.xaml new file mode 100644 index 000000000..a89f18c6f --- /dev/null +++ b/components/GradientSlider/samples/GradientSliderSample.xaml @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + diff --git a/components/GradientSlider/samples/GradientSliderSample.xaml.cs b/components/GradientSlider/samples/GradientSliderSample.xaml.cs new file mode 100644 index 000000000..6ab91ce52 --- /dev/null +++ b/components/GradientSlider/samples/GradientSliderSample.xaml.cs @@ -0,0 +1,21 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using CommunityToolkit.WinUI.Controls; + +namespace GradientSliderExperiment.Samples; + +/// +/// An example sample page of a custom control inheriting from Panel. +/// + +[ToolkitSample(id: nameof(GradientSliderSample), "Custom control", description: $"A sample for showing how to create and use a {nameof(GradientSlider)} custom control.")] +[ToolkitSampleBoolOption("CanAddStops", true, Title = nameof(GradientSlider.IsAddStopsEnabled))] +public sealed partial class GradientSliderSample : Page +{ + public GradientSliderSample() + { + this.InitializeComponent(); + } +} diff --git a/components/GradientSlider/src/CommunityToolkit.WinUI.Controls.GradientSlider.csproj b/components/GradientSlider/src/CommunityToolkit.WinUI.Controls.GradientSlider.csproj new file mode 100644 index 000000000..80ce29607 --- /dev/null +++ b/components/GradientSlider/src/CommunityToolkit.WinUI.Controls.GradientSlider.csproj @@ -0,0 +1,14 @@ + + + + + GradientSlider + This package contains GradientSlider. + + + CommunityToolkit.WinUI.Controls.GradientSliderRns + + + + + diff --git a/components/GradientSlider/src/Dependencies.props b/components/GradientSlider/src/Dependencies.props new file mode 100644 index 000000000..e622e1df4 --- /dev/null +++ b/components/GradientSlider/src/Dependencies.props @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/components/GradientSlider/src/GradientSlider.Events.cs b/components/GradientSlider/src/GradientSlider.Events.cs new file mode 100644 index 000000000..24939221f --- /dev/null +++ b/components/GradientSlider/src/GradientSlider.Events.cs @@ -0,0 +1,49 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace CommunityToolkit.WinUI.Controls; + +public partial class GradientSlider +{ + /// + /// Event raised when a thumb starts being dragged. + /// + public event DragStartedEventHandler? ThumbDragStarted; + + /// + /// Event raised when a thumb ends being dragged. + /// + public event DragCompletedEventHandler? ThumbDragCompleted; + + /// + /// Event raised when the gradient's value changes. + /// + public event EventHandler? ValueChanged; + + /// + /// Called before the event occurs. + /// + /// Event data for the event. + protected virtual void OnThumbDragStarted(DragStartedEventArgs e) + { + ThumbDragStarted?.Invoke(this, e); + } + + /// + /// Called before the event occurs. + /// + /// Event data for the event. + protected virtual void OnThumbDragCompleted(DragCompletedEventArgs e) + { + ThumbDragCompleted?.Invoke(this, e); + } + + /// + /// Called before the event occurs. + /// + protected virtual void OnValueChanged() + { + ValueChanged?.Invoke(this, EventArgs.Empty); + } +} diff --git a/components/GradientSlider/src/GradientSlider.Input.cs b/components/GradientSlider/src/GradientSlider.Input.cs new file mode 100644 index 000000000..a54444239 --- /dev/null +++ b/components/GradientSlider/src/GradientSlider.Input.cs @@ -0,0 +1,189 @@ +// 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 Windows.UI; +#else +using Microsoft.UI; +using Microsoft.UI.Xaml.Controls.Primitives; +#endif +using Windows.System; + +namespace CommunityToolkit.WinUI.Controls; + +public partial class GradientSlider +{ + private void Thumb_DragStarted(object sender, DragStartedEventArgs e) + { + if (sender is not GradientSliderThumb thumb) + return; + + _draggingThumb = thumb; + var xStart = Canvas.GetLeft(thumb); + var yStart = e.VerticalOffset; + _dragPosition = new Point(xStart, yStart); + + OnThumbDragStarted(e); + } + + private void Thumb_DragDelta(object sender, DragDeltaEventArgs e) + { + if (_containerCanvas is null) + return; + + if (sender is not GradientSliderThumb thumb) + return; + + _dragPosition.X += e.HorizontalChange; + _dragPosition.Y += e.VerticalChange; + + HandleThumbDragging(thumb, _dragPosition); + } + + private void Thumb_DragCompleted(object sender, DragCompletedEventArgs e) + { + _draggingThumb = null; + + OnThumbDragCompleted(e); + OnValueChanged(); + } + + private void Thumb_KeyDown(object sender, KeyRoutedEventArgs e) + { + if (sender is not GradientSliderThumb thumb) + return; + + var change = e.Key switch + { +#if !HAS_UNO + VirtualKey.Left when FlowDirection is FlowDirection.RightToLeft => 0.05, + VirtualKey.Right when FlowDirection is FlowDirection.RightToLeft => -0.05, +#endif + + VirtualKey.Left => -0.01, + VirtualKey.Right => 0.01, + + _ => 0, + }; + + if (change is not 0) + { + thumb.GradientStop.Offset = Math.Clamp(change + thumb.GradientStop.Offset, 0, 1); + UpdateThumbPosition(thumb); + e.Handled = true; + } + } + + private void ContainerCanvas_PointerEntered(object sender, PointerRoutedEventArgs e) + { + if (_placeholderThumb is null) + return; + + if (IsAddStopsEnabled) + { + _placeholderThumb.Visibility = Visibility.Visible; + } + + VisualStateManager.GoToState(this, PointerOverState, false); + } + + private void ContainerCanvas_PointerMoved(object sender, PointerRoutedEventArgs e) + { + if (_containerCanvas is null || _placeholderThumb is null) + return; + + var position = e.GetCurrentPoint(_containerCanvas).Position; + var posX = position.X; + + if (_draggingThumb is null) + { + // NOTE: This check could be made O(log(n)) by tracking the thumbs positions in a sorted list and running a binary search + _placeholderThumb.IsEnabled = !IsPointerOverThumb(posX); + + var thumbPosition = posX - _placeholderThumb.ActualWidth / 2; + thumbPosition = Math.Clamp(thumbPosition, 0, _containerCanvas.ActualWidth - _placeholderThumb.ActualWidth); + Canvas.SetLeft(_placeholderThumb, thumbPosition); + } + else if (_draggingThumb.PointerCaptures?.Count is null or 0) + { + HandleThumbDragging(_draggingThumb, position); + } + } + + private void ContainerCanvas_PointerExited(object sender, PointerRoutedEventArgs e) + { + if (_placeholderThumb is null) + return; + + _placeholderThumb.Visibility = Visibility.Collapsed; + _placeholderThumb.IsEnabled = false; + + VisualStateManager.GoToState(this, NormalState, false); + } + + private void ContainerCanvas_PointerPressed(object sender, PointerRoutedEventArgs e) + { + if (_containerCanvas is null || _placeholderThumb is null) + return; + + if (!IsAddStopsEnabled) + return; + + var position = e.GetCurrentPoint(_containerCanvas).Position.X; + if (IsPointerOverThumb(position)) + return; + + _containerCanvas.CapturePointer(e.Pointer); + + _placeholderThumb.IsEnabled = false; + + var stop = new GradientStop() + { + Offset = position / _containerCanvas.ActualWidth, + Color = Colors.Black, + }; + + GradientStops.Add(stop); + _draggingThumb = AddStop(stop); + } + + private void ContainerCanvas_PointerReleased(object sender, PointerRoutedEventArgs e) + { + if (_containerCanvas is null) + return; + + _draggingThumb = null; + _containerCanvas.ReleasePointerCapture(e.Pointer); + + OnValueChanged(); + } + + private bool IsPointerOverThumb(double position) + { + if (_containerCanvas is null) + return false; + + foreach (var child in _containerCanvas.Children) + { + if (child is not GradientSliderThumb thumb) + continue; + + var thumbPos = Canvas.GetLeft(thumb); + if (position > thumbPos - thumb.ActualWidth && position < thumbPos + (thumb.ActualWidth * 2)) + return true; + } + + return false; + } + + private void HandleThumbDragging(GradientSliderThumb thumb, Point position) + { + if (_containerCanvas is null) + return; + + var newPos = position.X / (_containerCanvas.ActualWidth - thumb.ActualWidth); + thumb.GradientStop.Offset = Math.Clamp(newPos, 0, 1); + UpdateThumbPosition(thumb); + } +} diff --git a/components/GradientSlider/src/GradientSlider.Properties.cs b/components/GradientSlider/src/GradientSlider.Properties.cs new file mode 100644 index 000000000..1ec8f5c4e --- /dev/null +++ b/components/GradientSlider/src/GradientSlider.Properties.cs @@ -0,0 +1,57 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace CommunityToolkit.WinUI.Controls; + +public partial class GradientSlider +{ + /// + /// The backing for the property. + /// + public static readonly DependencyProperty GradientStopsProperty = + DependencyProperty.Register(nameof(GradientStops), + typeof(GradientStopCollection), + typeof(GradientSlider), + new PropertyMetadata(null, GradientStopsChangedCallback)); + + /// + /// The backing for the property. + /// + public static readonly DependencyProperty IsAddStopsEnabledProperty = + DependencyProperty.Register(nameof(IsAddStopsEnabled), + typeof(bool), + typeof(GradientSlider), + new PropertyMetadata(true)); + + /// + /// Gets or sets the being modified by the . + /// + public GradientStopCollection GradientStops + { + get => (GradientStopCollection)GetValue(GradientStopsProperty); + set => SetValue(GradientStopsProperty, value); + } + + /// + /// Gets or sets whether or not the user can add new stops. + /// + public bool IsAddStopsEnabled + { + get => (bool)GetValue(IsAddStopsEnabledProperty); + set => SetValue(IsAddStopsEnabledProperty, value); + } + + private static void GradientStopsChangedCallback(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + if (d is not GradientSlider slider) + return; + + if (slider._containerCanvas is null) + return; + + // TODO: What happens if the gradient stop collection changes while the user is dragging a stop? + + slider.RefreshThumbs(); + } +} diff --git a/components/GradientSlider/src/GradientSlider.cs b/components/GradientSlider/src/GradientSlider.cs new file mode 100644 index 000000000..3cb22a5fe --- /dev/null +++ b/components/GradientSlider/src/GradientSlider.cs @@ -0,0 +1,203 @@ +// 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 Windows.UI; +using Windows.UI.Xaml.Shapes; +#else +using Microsoft.UI; +using Microsoft.UI.Xaml.Shapes; +#endif + +namespace CommunityToolkit.WinUI.Controls; + +/// +/// A slider for gradient color input. +/// +[TemplatePart(Name = "ContainerCanvas", Type = typeof(Canvas))] +[TemplatePart(Name = "PlaceholderThumb", Type = typeof(Thumb))] +[TemplatePart(Name = "BackgroundRectangle", Type = typeof(Rectangle))] +public partial class GradientSlider : Control +{ + internal const string CommonStates = "CommonStates"; + internal const string NormalState = "Normal"; + internal const string PointerOverState = "PointerOver"; + internal const string DisabledState = "Disabled"; + + private readonly Dictionary _stopThumbs = []; + private readonly Dictionary _stopCallbacks = []; + + private Canvas? _containerCanvas; + private Thumb? _placeholderThumb; + private Rectangle? _backgroundRectangle; + + private Point _dragPosition; + private GradientSliderThumb? _draggingThumb; + + /// + /// Creates a new instance of the class. + /// + public GradientSlider() + { + this.DefaultStyleKey = typeof(GradientSlider); + + GradientStops = + [ + new GradientStop + { + Color = Colors.Black, + Offset = 0, + }, + new GradientStop + { + Color = Colors.White, + Offset = 1, + } + ]; + } + + /// + protected override void OnApplyTemplate() + { + base.OnApplyTemplate(); + + if (_containerCanvas is not null) + { + _containerCanvas.SizeChanged -= ContainerCanvas_SizeChanged; + } + + _containerCanvas = (Canvas)GetTemplateChild("ContainerCanvas"); + _placeholderThumb = (Thumb)GetTemplateChild("PlaceholderThumb"); + _backgroundRectangle = (Rectangle?)GetTemplateChild("BackgroundRectangle"); + + _containerCanvas.SizeChanged += ContainerCanvas_SizeChanged; + + if (_placeholderThumb is not null) + { + _containerCanvas.PointerEntered += ContainerCanvas_PointerEntered; + _containerCanvas.PointerMoved += ContainerCanvas_PointerMoved; + _containerCanvas.PointerExited += ContainerCanvas_PointerExited; + _containerCanvas.PointerPressed += ContainerCanvas_PointerPressed; + _containerCanvas.PointerReleased += ContainerCanvas_PointerReleased; + + _placeholderThumb.Visibility = Visibility.Collapsed; + } + + RefreshThumbs(); + } + + private void ContainerCanvas_SizeChanged(object sender, SizeChangedEventArgs e) + => SyncThumbs(); + + private GradientSliderThumb? AddStop(GradientStop stop) + { + if (_containerCanvas is null) + { + throw new InvalidOperationException(nameof(_containerCanvas)); + } + + // Prepare a thumb for the gradient stop + var thumb = new GradientSliderThumb() + { + GradientStop = stop, + }; + + // Subcribe to events and callbacks + thumb.DragStarted += Thumb_DragStarted; + thumb.DragDelta += Thumb_DragDelta; + thumb.DragCompleted += Thumb_DragCompleted; + thumb.KeyDown += Thumb_KeyDown; + thumb.Loaded += Thumb_Loaded; + var callback = stop.RegisterPropertyChangedCallback(GradientStop.OffsetProperty, OnGradientStopOffsetChanged); + _stopCallbacks.Add(stop, callback); + + // Track the thumb and add to the canvas + _stopThumbs.Add(stop, thumb); + _containerCanvas.Children.Add(thumb); + + return thumb; + } + + private void RemoveStop(GradientStop stop) + { + if (_containerCanvas is null) + return; + + // Should this be an exception? + if (!_stopThumbs.TryGetValue(stop, out var thumb)) + return; + + // Unsubscribe from events and callbacks + thumb.DragStarted -= Thumb_DragStarted; + thumb.DragDelta -= Thumb_DragDelta; + thumb.DragCompleted -= Thumb_DragCompleted; + thumb.KeyDown -= Thumb_KeyDown; + thumb.Loaded -= Thumb_Loaded; + stop.UnregisterPropertyChangedCallback(GradientStop.OffsetProperty, _stopCallbacks[stop]); + _stopCallbacks.Remove(stop); + + // Untrack the thumb and remove from the canvas + _stopThumbs.Remove(stop); + _containerCanvas.Children.Remove(thumb); + } + + private void RefreshThumbs() + { + ClearThumbs(); + foreach (var stop in GradientStops) + { + AddStop(stop); + } + + SyncBackground(); + } + + private void ClearThumbs() + { + foreach (var (stop, _) in _stopThumbs) + RemoveStop(stop); + } + + private void SyncThumbs() + { + foreach (var thumb in _stopThumbs.Values) + UpdateThumbPosition(thumb); + } + + private void SyncBackground() + { + if (_containerCanvas is null || _backgroundRectangle is null) + return; + + _backgroundRectangle.Fill = new LinearGradientBrush(GradientStops, 0); + } + + private void Thumb_Loaded(object sender, RoutedEventArgs e) + { + if (sender is not GradientSliderThumb thumb) + return; + + // Thumb position cannot be determined until it has loaded. + // Defer until the loading event + thumb.Loaded -= Thumb_Loaded; + UpdateThumbPosition(thumb); + } + + private void OnGradientStopOffsetChanged(DependencyObject d, DependencyProperty e) + { + if (d is not GradientStop stop || !_stopThumbs.TryGetValue(stop, out var thumb)) + return; + + UpdateThumbPosition(thumb); + } + + private void UpdateThumbPosition(GradientSliderThumb thumb) + { + if (_containerCanvas is null) + return; + + var dragWidth = _containerCanvas.ActualWidth - thumb.ActualWidth; + Canvas.SetLeft(thumb, thumb.GradientStop.Offset * dragWidth); + } +} diff --git a/components/GradientSlider/src/GradientSliderStyle.xaml b/components/GradientSlider/src/GradientSliderStyle.xaml new file mode 100644 index 000000000..276d74293 --- /dev/null +++ b/components/GradientSlider/src/GradientSliderStyle.xaml @@ -0,0 +1,107 @@ + + + + + + 0.5 + + + + + + + + + + + + diff --git a/components/GradientSlider/src/GradientSliderThumb.Events.cs b/components/GradientSlider/src/GradientSliderThumb.Events.cs new file mode 100644 index 000000000..369de227e --- /dev/null +++ b/components/GradientSlider/src/GradientSliderThumb.Events.cs @@ -0,0 +1,121 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace CommunityToolkit.WinUI.Controls; + +public sealed partial class GradientSliderThumb : Control +{ + private bool _pointerOver; + private bool _pressed; + private bool _isDragging; + private Point _dragStartPosition; + private Point _lastPosition; + + /// + /// Fires when the captures the pointer. + /// + public event DragStartedEventHandler? DragStarted; + + /// + /// Fires as the is moved while capturing the pointer. + /// + public event DragDeltaEventHandler? DragDelta; + + /// + /// Fires when the releases the captured pointer. + /// + public event DragCompletedEventHandler? DragCompleted; + + private void GradientSliderThumb_PointerEntered(object sender, PointerRoutedEventArgs e) + { + _pointerOver = true; + + if (!_pressed) + { + VisualStateManager.GoToState(this, PointerOverState, true); + } + } + + private void GradientSliderThumb_PointerExited(object sender, PointerRoutedEventArgs e) + { + _pointerOver = false; + + if (!_pressed) + { + VisualStateManager.GoToState(this, NormalState, true); + } + } + + private void GradientSliderThumb_PointerPressed(object sender, PointerRoutedEventArgs e) + { + _pressed = true; + _isDragging = true; + + CapturePointer(e.Pointer); + + _dragStartPosition = e.GetCurrentPoint(null).Position; + _lastPosition = _dragStartPosition; + + DragStarted?.Invoke(this, + new DragStartedEventArgs(_dragStartPosition.X, _dragStartPosition.Y)); + + VisualStateManager.GoToState(this, PressedState, true); + } + + private void GradientSliderThumb_PointerMoved(object sender, PointerRoutedEventArgs e) + { + if (!_isDragging) + return; + + var position = e.GetCurrentPoint(null).Position; + + double deltaX = position.X - _lastPosition.X; + double deltaY = position.Y - _lastPosition.Y; + + _lastPosition = position; + + DragDelta?.Invoke(this, new DragDeltaEventArgs(deltaX, deltaY)); + } + + private void GradientSliderThumb_PointerReleased(object sender, PointerRoutedEventArgs e) + { + if (_isDragging) + { + var end = e.GetCurrentPoint(null).Position; + + double totalX = end.X - _dragStartPosition.X; + double totalY = end.Y - _dragStartPosition.Y; + + DragCompleted?.Invoke(this, + new DragCompletedEventArgs(totalX, totalY, false)); + } + + _isDragging = false; + _pressed = false; + + ReleasePointerCapture(e.Pointer); + + VisualStateManager.GoToState(this, _pointerOver ? PointerOverState : NormalState, true); + } + + private void GradientSliderThumb_PointerCanceled(object sender, PointerRoutedEventArgs e) + { + if (_isDragging) + { + DragCompleted?.Invoke(this, + new DragCompletedEventArgs(0, 0, true)); + } + + _isDragging = false; + _pressed = false; + + ReleasePointerCapture(e.Pointer); + VisualStateManager.GoToState(this, NormalState, true); + } + + private void GradientSliderThumb_IsEnabledChanged(object sender, DependencyPropertyChangedEventArgs e) + { + VisualStateManager.GoToState(this, IsEnabled ? NormalState : DisabledState, true); + } +} diff --git a/components/GradientSlider/src/GradientSliderThumb.cs b/components/GradientSlider/src/GradientSliderThumb.cs new file mode 100644 index 000000000..b8c637889 --- /dev/null +++ b/components/GradientSlider/src/GradientSliderThumb.cs @@ -0,0 +1,82 @@ +// 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 ColorPicker = Microsoft.UI.Xaml.Controls.ColorPicker; + +namespace CommunityToolkit.WinUI.Controls; + +/// +/// A thumb for adjusting a in a . +/// +[TemplatePart(Name = "PART_ColorPicker", Type = typeof(ColorPicker))] +[TemplatePart(Name = "PART_Border", Type = typeof(Border))] +public sealed partial class GradientSliderThumb : Control +{ + internal const string CommonStates = "CommonStates"; + internal const string NormalState = "Normal"; + internal const string PointerOverState = "PointerOver"; + internal const string PressedState = "Pressed"; + internal const string DisabledState = "Disabled"; + + private Border? _border; + private ColorPicker? _colorPicker; + + /// + /// The backing for the property. + /// + public static readonly DependencyProperty GradientStopProperty = + DependencyProperty.Register(nameof(GradientStop), + typeof(GradientStop), + typeof(GradientSliderThumb), + new PropertyMetadata(null)); + + /// + /// Initializes a new instance of the class. + /// + public GradientSliderThumb() + { + DefaultStyleKey = typeof(GradientSliderThumb); + } + + /// + /// Gets or sets the the thumb controls. + /// + public GradientStop GradientStop + { + get => (GradientStop)GetValue(GradientStopProperty); + set => SetValue(GradientStopProperty, value); + } + + /// + protected override void OnApplyTemplate() + { + base.OnApplyTemplate(); + + _border = (Border)GetTemplateChild("PART_Border"); + _colorPicker = (ColorPicker)GetTemplateChild("PART_ColorPicker"); + + PointerEntered += GradientSliderThumb_PointerEntered; + PointerExited += GradientSliderThumb_PointerExited; + PointerPressed += GradientSliderThumb_PointerPressed; + PointerMoved += GradientSliderThumb_PointerMoved; + PointerReleased += GradientSliderThumb_PointerReleased; + PointerCanceled += GradientSliderThumb_PointerCanceled; + IsEnabledChanged += GradientSliderThumb_IsEnabledChanged; + + _colorPicker.Color = GradientStop.Color; + _colorPicker.ColorChanged += ColorPicker_ColorChanged; + + Tapped += GradientSliderThumb_Tapped; + } + + private void ColorPicker_ColorChanged(ColorPicker sender, MUXC.ColorChangedEventArgs args) + { + GradientStop.Color = args.NewColor; + } + + private void GradientSliderThumb_Tapped(object sender, TappedRoutedEventArgs e) + { + FlyoutBase.ShowAttachedFlyout(_border); + } +} diff --git a/components/GradientSlider/src/GradientSliderThumbStyle.xaml b/components/GradientSlider/src/GradientSliderThumbStyle.xaml new file mode 100644 index 000000000..6541fdd27 --- /dev/null +++ b/components/GradientSlider/src/GradientSliderThumbStyle.xaml @@ -0,0 +1,126 @@ + + + + + + diff --git a/components/GradientSlider/src/MultiTarget.props b/components/GradientSlider/src/MultiTarget.props new file mode 100644 index 000000000..b11c19426 --- /dev/null +++ b/components/GradientSlider/src/MultiTarget.props @@ -0,0 +1,9 @@ + + + + uwp;wasdk;wpf;wasm;linuxgtk;macos;ios;android; + + \ No newline at end of file diff --git a/components/GradientSlider/src/Themes/Generic.xaml b/components/GradientSlider/src/Themes/Generic.xaml new file mode 100644 index 000000000..614177e26 --- /dev/null +++ b/components/GradientSlider/src/Themes/Generic.xaml @@ -0,0 +1,11 @@ + + + + + + + + + diff --git a/components/GradientSlider/tests/ExampleGradientSliderTestClass.cs b/components/GradientSlider/tests/ExampleGradientSliderTestClass.cs new file mode 100644 index 000000000..5b6462bba --- /dev/null +++ b/components/GradientSlider/tests/ExampleGradientSliderTestClass.cs @@ -0,0 +1,134 @@ +// 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.Tooling.TestGen; +using CommunityToolkit.Tests; +using CommunityToolkit.WinUI.Controls; + +namespace GradientSliderTests; + +[TestClass] +public partial class ExampleGradientSliderTestClass : VisualUITestBase +{ + // If you don't need access to UI objects directly or async code, use this pattern. + [TestMethod] + public void SimpleSynchronousExampleTest() + { + var assembly = typeof(GradientSlider).Assembly; + var type = assembly.GetType(typeof(GradientSlider).FullName ?? string.Empty); + + Assert.IsNotNull(type, "Could not find GradientSlider type."); + Assert.AreEqual(typeof(GradientSlider), type, "Type of GradientSlider does not match expected type."); + } + + // If you don't need access to UI objects directly, use this pattern. + [TestMethod] + public async Task SimpleAsyncExampleTest() + { + await Task.Delay(250); + + Assert.IsTrue(true); + } + + // Example that shows how to check for exception throwing. + [TestMethod] + public void SimpleExceptionCheckTest() + { + // If you need to check exceptions occur for invalid inputs, etc... + // Use Assert.ThrowsException to limit the scope to where you expect the error to occur. + // Otherwise, using the ExpectedException attribute could swallow or + // catch other issues in setup code. + Assert.ThrowsException(() => throw new NotImplementedException()); + } + + // The UIThreadTestMethod automatically dispatches to the UI for us to work with UI objects. + [UIThreadTestMethod] + public void SimpleUIAttributeExampleTest() + { + var component = new GradientSlider(); + Assert.IsNotNull(component); + } + + // The UIThreadTestMethod can also easily grab a XAML Page for us by passing its type as a parameter. + // This lets us actually test a control as it would behave within an actual application. + // The page will already be loaded by the time your test is called. + [UIThreadTestMethod] + public void SimpleUIExamplePageTest(ExampleGradientSliderTestPage page) + { + // You can use the Toolkit Visual Tree helpers here to find the component by type or name: + var component = page.FindDescendant(); + + Assert.IsNotNull(component); + + var componentByName = page.FindDescendant("GradientSliderControl"); + + Assert.IsNotNull(componentByName); + } + + // You can still do async work with a UIThreadTestMethod as well. + [UIThreadTestMethod] + public async Task SimpleAsyncUIExamplePageTest(ExampleGradientSliderTestPage page) + { + // This helper can be used to wait for a rendering pass to complete. + // Note, this is already done by loading a Page with the [UIThreadTestMethod] helper. + await CompositionTargetHelper.ExecuteAfterCompositionRenderingAsync(() => { }); + + var component = page.FindDescendant(); + + Assert.IsNotNull(component); + } + + //// ----------------------------- ADVANCED TEST SCENARIOS ----------------------------- + + // If you need to use DataRow, you can use this pattern with the UI dispatch still. + // Otherwise, checkout the UIThreadTestMethod attribute above. + // See https://github.com/CommunityToolkit/Labs-Windows/issues/186 + [TestMethod] + public async Task ComplexAsyncUIExampleTest() + { + await EnqueueAsync(() => + { + var component = new GradientSlider(); + Assert.IsNotNull(component); + }); + } + + // If you want to load other content not within a XAML page using the UIThreadTestMethod above. + // Then you can do that using the Load/UnloadTestContentAsync methods. + [TestMethod] + public async Task ComplexAsyncLoadUIExampleTest() + { + await EnqueueAsync(async () => + { + var component = new GradientSlider(); + Assert.IsNotNull(component); + Assert.IsFalse(component.IsLoaded); + + await LoadTestContentAsync(component); + + Assert.IsTrue(component.IsLoaded); + + await UnloadTestContentAsync(component); + + Assert.IsFalse(component.IsLoaded); + }); + } + + // You can still use the UIThreadTestMethod to remove the extra layer for the dispatcher as well: + [UIThreadTestMethod] + public async Task ComplexAsyncLoadUIExampleWithoutDispatcherTest() + { + var component = new GradientSlider(); + Assert.IsNotNull(component); + Assert.IsFalse(component.IsLoaded); + + await LoadTestContentAsync(component); + + Assert.IsTrue(component.IsLoaded); + + await UnloadTestContentAsync(component); + + Assert.IsFalse(component.IsLoaded); + } +} diff --git a/components/GradientSlider/tests/ExampleGradientSliderTestPage.xaml b/components/GradientSlider/tests/ExampleGradientSliderTestPage.xaml new file mode 100644 index 000000000..dbdc30833 --- /dev/null +++ b/components/GradientSlider/tests/ExampleGradientSliderTestPage.xaml @@ -0,0 +1,14 @@ + + + + + + + diff --git a/components/GradientSlider/tests/ExampleGradientSliderTestPage.xaml.cs b/components/GradientSlider/tests/ExampleGradientSliderTestPage.xaml.cs new file mode 100644 index 000000000..0fe89ed24 --- /dev/null +++ b/components/GradientSlider/tests/ExampleGradientSliderTestPage.xaml.cs @@ -0,0 +1,16 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace GradientSliderTests; + +/// +/// An empty page that can be used on its own or navigated to within a Frame. +/// +public sealed partial class ExampleGradientSliderTestPage : Page +{ + public ExampleGradientSliderTestPage() + { + this.InitializeComponent(); + } +} diff --git a/components/GradientSlider/tests/GradientSlider.Tests.projitems b/components/GradientSlider/tests/GradientSlider.Tests.projitems new file mode 100644 index 000000000..57a9b8fb5 --- /dev/null +++ b/components/GradientSlider/tests/GradientSlider.Tests.projitems @@ -0,0 +1,23 @@ + + + + $(MSBuildAllProjects);$(MSBuildThisFileFullPath) + true + 2F1B2F6D-D1F4-4895-96CE-F8CB3B3AF87F + + + GradientSliderTests + + + + + ExampleGradientSliderTestPage.xaml + + + + + Designer + MSBuild:Compile + + + \ No newline at end of file diff --git a/components/GradientSlider/tests/GradientSlider.Tests.shproj b/components/GradientSlider/tests/GradientSlider.Tests.shproj new file mode 100644 index 000000000..027ce84ad --- /dev/null +++ b/components/GradientSlider/tests/GradientSlider.Tests.shproj @@ -0,0 +1,13 @@ + + + + 2F1B2F6D-D1F4-4895-96CE-F8CB3B3AF87F + 14.0 + + + + + + + +