diff --git a/ColorPickerDemo/ColorPickerDemo.AvaloniaUI/ColorPickerDemo.AvaloniaUI.Android/ColorPickerDemo.AvaloniaUI.Android.csproj b/ColorPickerDemo/ColorPickerDemo.AvaloniaUI/ColorPickerDemo.AvaloniaUI.Android/ColorPickerDemo.AvaloniaUI.Android.csproj deleted file mode 100644 index 0e97056..0000000 --- a/ColorPickerDemo/ColorPickerDemo.AvaloniaUI/ColorPickerDemo.AvaloniaUI.Android/ColorPickerDemo.AvaloniaUI.Android.csproj +++ /dev/null @@ -1,27 +0,0 @@ - - - Exe - net7.0-android - 21 - enable - com.CompanyName.ColorPickerDemo.AvaloniaUI - 1 - 1.0 - apk - False - - - - - Resources\drawable\Icon.png - - - - - - - - - - - diff --git a/ColorPickerDemo/ColorPickerDemo.AvaloniaUI/ColorPickerDemo.AvaloniaUI.Android/Icon.png b/ColorPickerDemo/ColorPickerDemo.AvaloniaUI/ColorPickerDemo.AvaloniaUI.Android/Icon.png deleted file mode 100644 index 41a2a61..0000000 Binary files a/ColorPickerDemo/ColorPickerDemo.AvaloniaUI/ColorPickerDemo.AvaloniaUI.Android/Icon.png and /dev/null differ diff --git a/ColorPickerDemo/ColorPickerDemo.AvaloniaUI/ColorPickerDemo.AvaloniaUI.Android/MainActivity.cs b/ColorPickerDemo/ColorPickerDemo.AvaloniaUI/ColorPickerDemo.AvaloniaUI.Android/MainActivity.cs deleted file mode 100644 index ef0665c..0000000 --- a/ColorPickerDemo/ColorPickerDemo.AvaloniaUI/ColorPickerDemo.AvaloniaUI.Android/MainActivity.cs +++ /dev/null @@ -1,12 +0,0 @@ -using Android.App; -using Android.Content.PM; -using Avalonia.Android; - -namespace ColorPickerDemo.AvaloniaUI.Android; - -[Activity(Label = "ColorPickerDemo.AvaloniaUI.Android", Theme = "@style/MyTheme.NoActionBar", Icon = "@drawable/icon", - LaunchMode = LaunchMode.SingleTop, - ConfigurationChanges = ConfigChanges.Orientation | ConfigChanges.ScreenSize | ConfigChanges.UiMode)] -public class MainActivity : AvaloniaMainActivity -{ -} \ No newline at end of file diff --git a/ColorPickerDemo/ColorPickerDemo.AvaloniaUI/ColorPickerDemo.AvaloniaUI.Android/Properties/AndroidManifest.xml b/ColorPickerDemo/ColorPickerDemo.AvaloniaUI/ColorPickerDemo.AvaloniaUI.Android/Properties/AndroidManifest.xml deleted file mode 100644 index 37633bb..0000000 --- a/ColorPickerDemo/ColorPickerDemo.AvaloniaUI/ColorPickerDemo.AvaloniaUI.Android/Properties/AndroidManifest.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/ColorPickerDemo/ColorPickerDemo.AvaloniaUI/ColorPickerDemo.AvaloniaUI.Android/Resources/drawable/splash_screen.xml b/ColorPickerDemo/ColorPickerDemo.AvaloniaUI/ColorPickerDemo.AvaloniaUI.Android/Resources/drawable/splash_screen.xml deleted file mode 100644 index 2e920b4..0000000 --- a/ColorPickerDemo/ColorPickerDemo.AvaloniaUI/ColorPickerDemo.AvaloniaUI.Android/Resources/drawable/splash_screen.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - - - diff --git a/ColorPickerDemo/ColorPickerDemo.AvaloniaUI/ColorPickerDemo.AvaloniaUI.Android/Resources/values-night/colors.xml b/ColorPickerDemo/ColorPickerDemo.AvaloniaUI/ColorPickerDemo.AvaloniaUI.Android/Resources/values-night/colors.xml deleted file mode 100644 index 3d47b6f..0000000 --- a/ColorPickerDemo/ColorPickerDemo.AvaloniaUI/ColorPickerDemo.AvaloniaUI.Android/Resources/values-night/colors.xml +++ /dev/null @@ -1,4 +0,0 @@ - - - #212121 - diff --git a/ColorPickerDemo/ColorPickerDemo.AvaloniaUI/ColorPickerDemo.AvaloniaUI.Android/Resources/values/colors.xml b/ColorPickerDemo/ColorPickerDemo.AvaloniaUI/ColorPickerDemo.AvaloniaUI.Android/Resources/values/colors.xml deleted file mode 100644 index 59279d5..0000000 --- a/ColorPickerDemo/ColorPickerDemo.AvaloniaUI/ColorPickerDemo.AvaloniaUI.Android/Resources/values/colors.xml +++ /dev/null @@ -1,4 +0,0 @@ - - - #FFFFFF - diff --git a/ColorPickerDemo/ColorPickerDemo.AvaloniaUI/ColorPickerDemo.AvaloniaUI.Android/Resources/values/styles.xml b/ColorPickerDemo/ColorPickerDemo.AvaloniaUI/ColorPickerDemo.AvaloniaUI.Android/Resources/values/styles.xml deleted file mode 100644 index 2759d29..0000000 --- a/ColorPickerDemo/ColorPickerDemo.AvaloniaUI/ColorPickerDemo.AvaloniaUI.Android/Resources/values/styles.xml +++ /dev/null @@ -1,17 +0,0 @@ - - - - - - - - - - diff --git a/ColorPickerDemo/ColorPickerDemo.AvaloniaUI/ColorPickerDemo.AvaloniaUI.Android/SplashActivity.cs b/ColorPickerDemo/ColorPickerDemo.AvaloniaUI/ColorPickerDemo.AvaloniaUI.Android/SplashActivity.cs deleted file mode 100644 index eef6cf9..0000000 --- a/ColorPickerDemo/ColorPickerDemo.AvaloniaUI/ColorPickerDemo.AvaloniaUI.Android/SplashActivity.cs +++ /dev/null @@ -1,32 +0,0 @@ -using Android.App; -using Android.Content; -using Android.OS; -using Application = Android.App.Application; -using Avalonia; -using Avalonia.Android; -using Avalonia.ReactiveUI; - -namespace ColorPickerDemo.AvaloniaUI.Android; - -[Activity(Theme = "@style/MyTheme.Splash", MainLauncher = true, NoHistory = true)] -public class SplashActivity : AvaloniaSplashActivity -{ - protected override AppBuilder CustomizeAppBuilder(AppBuilder builder) - { - return base.CustomizeAppBuilder(builder) - .WithInterFont() - .UseReactiveUI(); - } - - protected override void OnCreate(Bundle? savedInstanceState) - { - base.OnCreate(savedInstanceState); - } - - protected override void OnResume() - { - base.OnResume(); - - StartActivity(new Intent(Application.Context, typeof(MainActivity))); - } -} \ No newline at end of file diff --git a/ColorPickerDemo/ColorPickerDemo.AvaloniaUI/ColorPickerDemo.AvaloniaUI.iOS/AppDelegate.cs b/ColorPickerDemo/ColorPickerDemo.AvaloniaUI/ColorPickerDemo.AvaloniaUI.iOS/AppDelegate.cs deleted file mode 100644 index eca7f3c..0000000 --- a/ColorPickerDemo/ColorPickerDemo.AvaloniaUI/ColorPickerDemo.AvaloniaUI.iOS/AppDelegate.cs +++ /dev/null @@ -1,23 +0,0 @@ -using Foundation; -using UIKit; -using Avalonia; -using Avalonia.Controls; -using Avalonia.iOS; -using Avalonia.Media; -using Avalonia.ReactiveUI; - -namespace ColorPickerDemo.AvaloniaUI.iOS; - -// The UIApplicationDelegate for the application. This class is responsible for launching the -// User Interface of the application, as well as listening (and optionally responding) to -// application events from iOS. -[Register("AppDelegate")] -public partial class AppDelegate : AvaloniaAppDelegate -{ - protected override AppBuilder CustomizeAppBuilder(AppBuilder builder) - { - return base.CustomizeAppBuilder(builder) - .WithInterFont() - .UseReactiveUI(); - } -} \ No newline at end of file diff --git a/ColorPickerDemo/ColorPickerDemo.AvaloniaUI/ColorPickerDemo.AvaloniaUI.iOS/ColorPickerDemo.AvaloniaUI.iOS.csproj b/ColorPickerDemo/ColorPickerDemo.AvaloniaUI/ColorPickerDemo.AvaloniaUI.iOS/ColorPickerDemo.AvaloniaUI.iOS.csproj deleted file mode 100644 index cd3bf8e..0000000 --- a/ColorPickerDemo/ColorPickerDemo.AvaloniaUI/ColorPickerDemo.AvaloniaUI.iOS/ColorPickerDemo.AvaloniaUI.iOS.csproj +++ /dev/null @@ -1,22 +0,0 @@ - - - Exe - net7.0-ios - 10.0 - manual - enable - iossimulator-x64 - - - - - - - - - - - - - - diff --git a/ColorPickerDemo/ColorPickerDemo.AvaloniaUI/ColorPickerDemo.AvaloniaUI.iOS/Entitlements.plist b/ColorPickerDemo/ColorPickerDemo.AvaloniaUI/ColorPickerDemo.AvaloniaUI.iOS/Entitlements.plist deleted file mode 100644 index 0c67376..0000000 --- a/ColorPickerDemo/ColorPickerDemo.AvaloniaUI/ColorPickerDemo.AvaloniaUI.iOS/Entitlements.plist +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/ColorPickerDemo/ColorPickerDemo.AvaloniaUI/ColorPickerDemo.AvaloniaUI.iOS/Info.plist b/ColorPickerDemo/ColorPickerDemo.AvaloniaUI/ColorPickerDemo.AvaloniaUI.iOS/Info.plist deleted file mode 100644 index eb7ed6e..0000000 --- a/ColorPickerDemo/ColorPickerDemo.AvaloniaUI/ColorPickerDemo.AvaloniaUI.iOS/Info.plist +++ /dev/null @@ -1,47 +0,0 @@ - - - - - CFBundleDisplayName - ColorPickerDemo.AvaloniaUI - CFBundleIdentifier - companyName.ColorPickerDemo.AvaloniaUI - CFBundleShortVersionString - 1.0 - CFBundleVersion - 1.0 - LSRequiresIPhoneOS - - MinimumOSVersion - 10.0 - UIDeviceFamily - - 1 - 2 - - UILaunchStoryboardName - LaunchScreen - UIRequiredDeviceCapabilities - - armv7 - - UISupportedInterfaceOrientations - - UIInterfaceOrientationPortrait - UIInterfaceOrientationPortraitUpsideDown - UIInterfaceOrientationLandscapeLeft - UIInterfaceOrientationLandscapeRight - - UISupportedInterfaceOrientations~ipad - - UIInterfaceOrientationPortrait - UIInterfaceOrientationPortraitUpsideDown - UIInterfaceOrientationLandscapeLeft - UIInterfaceOrientationLandscapeRight - - UIStatusBarHidden - - UIViewControllerBasedStatusBarAppearance - - - diff --git a/ColorPickerDemo/ColorPickerDemo.AvaloniaUI/ColorPickerDemo.AvaloniaUI.iOS/Main.cs b/ColorPickerDemo/ColorPickerDemo.AvaloniaUI/ColorPickerDemo.AvaloniaUI.iOS/Main.cs deleted file mode 100644 index 0889a07..0000000 --- a/ColorPickerDemo/ColorPickerDemo.AvaloniaUI/ColorPickerDemo.AvaloniaUI.iOS/Main.cs +++ /dev/null @@ -1,14 +0,0 @@ -using UIKit; - -namespace ColorPickerDemo.AvaloniaUI.iOS; - -public class Application -{ - // This is the main entry point of the application. - static void Main(string[] args) - { - // if you want to use a different Application Delegate class from "AppDelegate" - // you can specify it here. - UIApplication.Main(args, null, typeof(AppDelegate)); - } -} \ No newline at end of file diff --git a/ColorPickerDemo/ColorPickerDemo.AvaloniaUI/ColorPickerDemo.AvaloniaUI.iOS/Resources/LaunchScreen.xib b/ColorPickerDemo/ColorPickerDemo.AvaloniaUI/ColorPickerDemo.AvaloniaUI.iOS/Resources/LaunchScreen.xib deleted file mode 100644 index acfef1a..0000000 --- a/ColorPickerDemo/ColorPickerDemo.AvaloniaUI/ColorPickerDemo.AvaloniaUI.iOS/Resources/LaunchScreen.xib +++ /dev/null @@ -1,43 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/ColorPickerDemo/ColorPickerDemo.AvaloniaUI/ColorPickerDemo.AvaloniaUI/Views/MainView.axaml b/ColorPickerDemo/ColorPickerDemo.AvaloniaUI/ColorPickerDemo.AvaloniaUI/Views/MainView.axaml index 5e2d446..c611233 100644 --- a/ColorPickerDemo/ColorPickerDemo.AvaloniaUI/ColorPickerDemo.AvaloniaUI/Views/MainView.axaml +++ b/ColorPickerDemo/ColorPickerDemo.AvaloniaUI/ColorPickerDemo.AvaloniaUI/Views/MainView.axaml @@ -25,11 +25,13 @@ Portable Color Picker, alpha hidden, transparent hint color, hsv/hsl fractional part hidden @@ -64,7 +67,7 @@ HSV @@ -76,7 +79,7 @@ RGBA diff --git a/ColorPickerDemo/ColorPickerDemo.AvaloniaUI/Directory.Build.props b/ColorPickerDemo/ColorPickerDemo.AvaloniaUI/Directory.Build.props index 10ab2cd..4286ecd 100644 --- a/ColorPickerDemo/ColorPickerDemo.AvaloniaUI/Directory.Build.props +++ b/ColorPickerDemo/ColorPickerDemo.AvaloniaUI/Directory.Build.props @@ -1,6 +1,6 @@ enable - 11.2.5 + 11.3.0 diff --git a/ColorPickerDemo/stylecop.json b/ColorPickerDemo/stylecop.json new file mode 100644 index 0000000..e69de29 diff --git a/src/ColorPicker.AvaloniaUI/Behaviors/HexTextBoxBindingBehavior.cs b/src/ColorPicker.AvaloniaUI/Behaviors/HexTextBoxBindingBehavior.cs index 5805146..ba99d01 100644 --- a/src/ColorPicker.AvaloniaUI/Behaviors/HexTextBoxBindingBehavior.cs +++ b/src/ColorPicker.AvaloniaUI/Behaviors/HexTextBoxBindingBehavior.cs @@ -7,8 +7,9 @@ namespace ColorPicker.Behaviors; internal class HexTextBoxBindingBehavior : LostFocusUpdateBindingBehavior { - public static readonly StyledProperty ColorProperty = AvaloniaProperty.Register( - "Color"); + public static readonly StyledProperty ColorProperty = + AvaloniaProperty.Register( + "Color"); public Color Color { @@ -16,8 +17,9 @@ public Color Color set => SetValue(ColorProperty, value); } - public static readonly StyledProperty HexConverterProperty = AvaloniaProperty.Register( - "HexConverter"); + public static readonly StyledProperty HexConverterProperty = + AvaloniaProperty.Register( + "HexConverter"); public ColorToHexConverter HexConverter { @@ -36,4 +38,4 @@ protected override void OnSubmitValue(string oldValue, string newValue) Color = color; } -} \ No newline at end of file +} diff --git a/src/ColorPicker.AvaloniaUI/Behaviors/LostFocusUpdateBindingBehavior.cs b/src/ColorPicker.AvaloniaUI/Behaviors/LostFocusUpdateBindingBehavior.cs index 91ba16d..0e169bc 100644 --- a/src/ColorPicker.AvaloniaUI/Behaviors/LostFocusUpdateBindingBehavior.cs +++ b/src/ColorPicker.AvaloniaUI/Behaviors/LostFocusUpdateBindingBehavior.cs @@ -30,6 +30,7 @@ public string Text protected override void OnAttached() { AssociatedObject.LostFocus += OnLostFocus; + OnBindingValueChanged(); base.OnAttached(); } @@ -49,7 +50,7 @@ private void OnLostFocus(object sender, RoutedEventArgs e) } } - private void OnBindingValueChanged() + protected void OnBindingValueChanged() { if (AssociatedObject != null) { @@ -61,4 +62,4 @@ protected virtual void OnSubmitValue(string oldValue, string newValue) { } -} \ No newline at end of file +} diff --git a/src/ColorPicker.AvaloniaUI/Behaviors/TextBoxFocusBehavior.cs b/src/ColorPicker.AvaloniaUI/Behaviors/TextBoxFocusBehavior.cs index 164494b..ad3b3d0 100644 --- a/src/ColorPicker.AvaloniaUI/Behaviors/TextBoxFocusBehavior.cs +++ b/src/ColorPicker.AvaloniaUI/Behaviors/TextBoxFocusBehavior.cs @@ -7,7 +7,7 @@ namespace ColorPicker.Behaviors; -internal class TextBoxFocusBehavior : Behavior +public class TextBoxFocusBehavior : Behavior { public static readonly StyledProperty SelectOnMouseClickProperty = AvaloniaProperty.Register( @@ -119,4 +119,4 @@ private void AssociatedObjectPointerPressed(object sender, PointerPressedEventAr e.Handled = true; } } -} \ No newline at end of file +} diff --git a/src/ColorPicker.AvaloniaUI/ColorPicker.AvaloniaUI.csproj b/src/ColorPicker.AvaloniaUI/ColorPicker.AvaloniaUI.csproj index 76ef22a..5314ccb 100644 --- a/src/ColorPicker.AvaloniaUI/ColorPicker.AvaloniaUI.csproj +++ b/src/ColorPicker.AvaloniaUI/ColorPicker.AvaloniaUI.csproj @@ -1,7 +1,7 @@ - net6.0;net7.0;net8.0 + net8.0 enable disable true @@ -31,8 +31,9 @@ Originally developed for PixiEditor: https://github.com/PixiEditor/PixiEditor. - - + + + @@ -69,4 +70,10 @@ Originally developed for PixiEditor: https://github.com/PixiEditor/PixiEditor. + + + + ..\..\..\..\.nuget\packages\avalonia\11.0.5\ref\net6.0\Avalonia.Base.dll + + diff --git a/src/ColorPicker.AvaloniaUI/ConicGradientPad.cs b/src/ColorPicker.AvaloniaUI/ConicGradientPad.cs new file mode 100644 index 0000000..fc3df0f --- /dev/null +++ b/src/ColorPicker.AvaloniaUI/ConicGradientPad.cs @@ -0,0 +1,100 @@ +using System.ComponentModel; +using Avalonia; +using Avalonia.Controls; +using Avalonia.Controls.Metadata; +using Avalonia.Controls.Primitives; +using Avalonia.Input; + +namespace ColorPicker; + +[TemplatePart("PART_CenterHandle", typeof(Control))] +[TemplatePart("PART_AngleHandle", typeof(Control))] +public class ConicGradientPad : GradientPad, INotifyPropertyChanged +{ + public static readonly StyledProperty CenterXProperty = AvaloniaProperty.Register( + nameof(CenterX)); + + public static readonly StyledProperty CenterYProperty = AvaloniaProperty.Register( + nameof(CenterY)); + + public static readonly StyledProperty AngleProperty = AvaloniaProperty.Register( + nameof(Angle)); + + public double Angle + { + get => GetValue(AngleProperty); + set => SetValue(AngleProperty, value); + } + + public double CenterY + { + get => GetValue(CenterYProperty); + set => SetValue(CenterYProperty, value); + } + + public double CenterX + { + get => GetValue(CenterXProperty); + set => SetValue(CenterXProperty, value); + } + + public double EndPointX => CenterX + Math.Cos((Angle - 90) * Math.PI / 180); + public double EndPointY => CenterY + Math.Sin((Angle - 90) * Math.PI / 180); + + private Cursor westEastCursor = new Cursor(StandardCursorType.SizeWestEast); + private Cursor northSouthCursor = new Cursor(StandardCursorType.SizeNorthSouth); + + public Cursor CurrentAngleCursor => Angle is > 45 and < 135 || Angle is > 225 and < 315 ? northSouthCursor : westEastCursor; + + public event PropertyChangedEventHandler PropertyChanged; + + private Control centerHandle; + private Control angleHandle; + + static ConicGradientPad() + { + AngleProperty.Changed.Subscribe(UpdateEnd); + CenterXProperty.Changed.Subscribe(UpdateEnd); + CenterYProperty.Changed.Subscribe(UpdateEnd); + } + + protected override void OnApplyTemplate(TemplateAppliedEventArgs e) + { + base.OnApplyTemplate(e); + centerHandle = e.NameScope.Get("PART_CenterHandle"); + angleHandle = e.NameScope.Get("PART_AngleHandle"); + + AddHandle(centerHandle, (x, y) => + { + CenterX = x; + CenterY = y; + + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(EndPointX))); + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(EndPointY))); + }); + + AddHandle(angleHandle, (x, y) => + { + var angle = Math.Atan2(y - CenterY, x - CenterX) * 180 / Math.PI + 90; + if (angle < 0) + { + angle += 360; + } + + Angle = angle; + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(EndPointX))); + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(EndPointY))); + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(CurrentAngleCursor))); + }); + } + + private static void UpdateEnd(AvaloniaPropertyChangedEventArgs e) + { + if (e.Sender is ConicGradientPad pad) + { + pad.PropertyChanged?.Invoke(pad, new PropertyChangedEventArgs(nameof(EndPointX))); + pad.PropertyChanged?.Invoke(pad, new PropertyChangedEventArgs(nameof(EndPointY))); + pad.PropertyChanged?.Invoke(pad, new PropertyChangedEventArgs(nameof(CurrentAngleCursor))); + } + } +} diff --git a/src/ColorPicker.AvaloniaUI/Converters/PickerTypeToIntConverter.cs b/src/ColorPicker.AvaloniaUI/Converters/EnumTypeToIntConverter.cs similarity index 68% rename from src/ColorPicker.AvaloniaUI/Converters/PickerTypeToIntConverter.cs rename to src/ColorPicker.AvaloniaUI/Converters/EnumTypeToIntConverter.cs index 3fbddd2..5de9dd0 100644 --- a/src/ColorPicker.AvaloniaUI/Converters/PickerTypeToIntConverter.cs +++ b/src/ColorPicker.AvaloniaUI/Converters/EnumTypeToIntConverter.cs @@ -4,7 +4,7 @@ namespace ColorPicker.Converters; -internal class PickerTypeToIntConverter +internal class EnumTypeToIntConverter : IValueConverter { public object Convert(object value, Type targetType, object parameter, CultureInfo culture) @@ -14,6 +14,11 @@ public object Convert(object value, Type targetType, object parameter, CultureIn public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) { - return (PickerType)value; + if (parameter is Type targetEnumType) + { + return Enum.ToObject(targetEnumType, value); + } + + return value; } } \ No newline at end of file diff --git a/src/ColorPicker.AvaloniaUI/Converters/EqualsConverter.cs b/src/ColorPicker.AvaloniaUI/Converters/EqualsConverter.cs new file mode 100644 index 0000000..204847f --- /dev/null +++ b/src/ColorPicker.AvaloniaUI/Converters/EqualsConverter.cs @@ -0,0 +1,17 @@ +using System.Globalization; +using Avalonia.Data.Converters; + +namespace ColorPicker.Converters; + +internal class EqualsConverter : IMultiValueConverter +{ + public object Convert(IList values, Type targetType, object parameter, CultureInfo culture) + { + if (values.Count != 2) + { + return false; + } + + return values[0]?.Equals(values[1]) ?? false; + } +} \ No newline at end of file diff --git a/src/ColorPicker.AvaloniaUI/Converters/GradientStopToColorConverter.cs b/src/ColorPicker.AvaloniaUI/Converters/GradientStopToColorConverter.cs new file mode 100644 index 0000000..ed1d98b --- /dev/null +++ b/src/ColorPicker.AvaloniaUI/Converters/GradientStopToColorConverter.cs @@ -0,0 +1,23 @@ +using System.Globalization; +using Avalonia.Data.Converters; +using Avalonia.Media; + +namespace ColorPicker.Converters; + +public class GradientStopToColorConverter : IValueConverter +{ + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + if (value is GradientStop stop) + { + return stop.Color; + } + + return Colors.Transparent; + } + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + { + throw new NotImplementedException(); + } +} diff --git a/src/ColorPicker.AvaloniaUI/Converters/IsEnumValueConverter.cs b/src/ColorPicker.AvaloniaUI/Converters/IsEnumValueConverter.cs new file mode 100644 index 0000000..d9aa753 --- /dev/null +++ b/src/ColorPicker.AvaloniaUI/Converters/IsEnumValueConverter.cs @@ -0,0 +1,22 @@ +using System.Globalization; +using Avalonia.Data.Converters; + +namespace ColorPicker.Converters; + +public class IsEnumValueConverter : IValueConverter +{ + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + if (parameter is not null && value is not null) + { + return string.Equals(value.ToString(), parameter.ToString(), StringComparison.InvariantCultureIgnoreCase); + } + + return false; + } + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + { + throw new NotImplementedException(); + } +} diff --git a/src/ColorPicker.AvaloniaUI/Converters/LastItemOfConverter.cs b/src/ColorPicker.AvaloniaUI/Converters/LastItemOfConverter.cs new file mode 100644 index 0000000..43723ac --- /dev/null +++ b/src/ColorPicker.AvaloniaUI/Converters/LastItemOfConverter.cs @@ -0,0 +1,23 @@ +using System.Collections; +using System.Globalization; +using Avalonia.Data.Converters; + +namespace ColorPicker.Converters; + +public class LastItemOfConverter : IValueConverter +{ + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + if (value is not IList list || list.Count == 0) + { + return null; + } + + return list[^1]; + } + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + { + throw new NotImplementedException(); + } +} diff --git a/src/ColorPicker.AvaloniaUI/Converters/MultiplyConverter.cs b/src/ColorPicker.AvaloniaUI/Converters/MultiplyConverter.cs new file mode 100644 index 0000000..4d32505 --- /dev/null +++ b/src/ColorPicker.AvaloniaUI/Converters/MultiplyConverter.cs @@ -0,0 +1,27 @@ +using System.Globalization; +using Avalonia; +using Avalonia.Data.Converters; + +namespace ColorPicker.Converters; + +public class MultiplyConverter : IMultiValueConverter +{ + public object Convert(IList values, Type targetType, object parameter, CultureInfo culture) + { + if (values.Count < 2) + { + return AvaloniaProperty.UnsetValue; + } + + double result = 1.0; + foreach (var value in values) + { + if (value is double d) + { + result *= d; + } + } + + return result; + } +} diff --git a/src/ColorPicker.AvaloniaUI/Converters/NotNullConverter.cs b/src/ColorPicker.AvaloniaUI/Converters/NotNullConverter.cs new file mode 100644 index 0000000..6e57482 --- /dev/null +++ b/src/ColorPicker.AvaloniaUI/Converters/NotNullConverter.cs @@ -0,0 +1,17 @@ +using System.Globalization; +using Avalonia.Data.Converters; + +namespace ColorPicker.Converters; + +public class NotNullConverter : IValueConverter +{ + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + return value != null; + } + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + { + throw new NotImplementedException(); + } +} diff --git a/src/ColorPicker.AvaloniaUI/Converters/OffsetToMarginConverter.cs b/src/ColorPicker.AvaloniaUI/Converters/OffsetToMarginConverter.cs new file mode 100644 index 0000000..48d20f6 --- /dev/null +++ b/src/ColorPicker.AvaloniaUI/Converters/OffsetToMarginConverter.cs @@ -0,0 +1,27 @@ +using System.Globalization; +using Avalonia; +using Avalonia.Data.Converters; + +namespace ColorPicker.Converters; + +public class OffsetToMarginConverter : IMultiValueConverter +{ + public object Convert(IList values, Type targetType, object parameter, CultureInfo culture) + { + if (values.Count != 2) + { + return new Thickness(); + } + + if (!(values[0] is double offset) || !(values[1] is double width)) + { + return new Thickness(); + } + + double padding = parameter is double p ? p : 0; + + padding = offset < 0.5 ? 0 : -padding; + + return new Thickness(offset * width, 0, 0, 0) + new Thickness(padding, 0, 0, 0); + } +} \ No newline at end of file diff --git a/src/ColorPicker.AvaloniaUI/Converters/PointComponentsToPointConverter.cs b/src/ColorPicker.AvaloniaUI/Converters/PointComponentsToPointConverter.cs new file mode 100644 index 0000000..8bb72ff --- /dev/null +++ b/src/ColorPicker.AvaloniaUI/Converters/PointComponentsToPointConverter.cs @@ -0,0 +1,22 @@ +using System.Globalization; +using Avalonia; +using Avalonia.Data.Converters; + +namespace ColorPicker.Converters; + +public class PointComponentsToPointConverter : IMultiValueConverter +{ + public object Convert(IList values, Type targetType, object parameter, CultureInfo culture) + { + if (values.Count < 3 || values[0] is not double x || values[1] is not double y || values[2] is not Rect rect) + { + return new Point(0, 0); + } + + Point point = new Point( + x * rect.Width, + y * rect.Height); + + return point; + } +} diff --git a/src/ColorPicker.AvaloniaUI/Converters/PointsToMarginConverter.cs b/src/ColorPicker.AvaloniaUI/Converters/PointsToMarginConverter.cs new file mode 100644 index 0000000..6466d95 --- /dev/null +++ b/src/ColorPicker.AvaloniaUI/Converters/PointsToMarginConverter.cs @@ -0,0 +1,23 @@ +using System.Globalization; +using Avalonia; +using Avalonia.Data.Converters; + +namespace ColorPicker.Converters; + +public class PointsToMarginConverter : IMultiValueConverter +{ + public object Convert(IList values, Type targetType, object parameter, CultureInfo culture) + { + if (values.Count < 3 || values[0] is not double x || values[1] is not double y || values[2] is not Rect rect) + { + return new Thickness(0); + } + + Thickness margin = new Thickness( + x * rect.Width, + y * rect.Height, + 0, 0); + + return margin; + } +} diff --git a/src/ColorPicker.AvaloniaUI/Converters/RangeConstrainedDoubleToDoubleConverter.cs b/src/ColorPicker.AvaloniaUI/Converters/RangeConstrainedDoubleToDoubleConverter.cs index 64d14b9..76b4ea4 100644 --- a/src/ColorPicker.AvaloniaUI/Converters/RangeConstrainedDoubleToDoubleConverter.cs +++ b/src/ColorPicker.AvaloniaUI/Converters/RangeConstrainedDoubleToDoubleConverter.cs @@ -8,6 +8,9 @@ namespace ColorPicker.Converters; internal class RangeConstrainedDoubleToDoubleConverter : AvaloniaObject, IValueConverter { + public static readonly StyledProperty OriginalValueProperty = + AvaloniaProperty.Register("OriginalValue"); + public static readonly StyledProperty MinProperty = AvaloniaProperty.Register( nameof(Min)); @@ -28,8 +31,9 @@ public double Max set => SetValue(MaxProperty, value); } - public static readonly StyledProperty ShowFractionalPartProperty = AvaloniaProperty.Register( - nameof(ShowFractionalPart), true); + public static readonly StyledProperty ShowFractionalPartProperty = + AvaloniaProperty.Register( + nameof(ShowFractionalPart), true); public bool ShowFractionalPart { @@ -37,6 +41,12 @@ public bool ShowFractionalPart set => SetValue(ShowFractionalPartProperty, value); } + public double OriginalValue + { + get { return (double)GetValue(OriginalValueProperty); } + set { SetValue(OriginalValueProperty, value); } + } + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) { @@ -54,6 +64,14 @@ public object ConvertBack(object value, Type targetType, object parameter, Cultu if (!double.TryParse(((string)value).Replace(',', '.'), NumberStyles.Float, CultureInfo.InvariantCulture, out var result)) return AvaloniaProperty.UnsetValue; - return MathHelper.Clamp(result, Min, Max); + + var clampedValue = MathHelper.Clamp(result, Min, Max); + + if (Math.Abs(clampedValue - OriginalValue) < 0.1) + { + return OriginalValue; + } + + return clampedValue; } -} \ No newline at end of file +} diff --git a/src/ColorPicker.AvaloniaUI/DualColorGradientPickerBase.cs b/src/ColorPicker.AvaloniaUI/DualColorGradientPickerBase.cs new file mode 100644 index 0000000..2bb1909 --- /dev/null +++ b/src/ColorPicker.AvaloniaUI/DualColorGradientPickerBase.cs @@ -0,0 +1,353 @@ +using Avalonia; +using Avalonia.Media; +using ColorPicker.Models; +using GradientStop = ColorPicker.Models.GradientStop; +using Matrix = ColorPicker.Models.Matrix; + +namespace ColorPicker; + +public class DualColorGradientPickerBase : DualPickerControlBase, IGradientStorage +{ + public static readonly StyledProperty EnableGradientsTabProperty = + AvaloniaProperty.Register( + nameof(EnableGradientsTab), true); + + public static readonly StyledProperty GradientStateProperty = + AvaloniaProperty.Register( + nameof(GradientState)); + + public static readonly StyledProperty GradientBrushProperty = + AvaloniaProperty.Register( + nameof(GradientBrush)); + + public static readonly StyledProperty SelectedTabIndexProperty = + AvaloniaProperty.Register( + nameof(SelectedTabIndex)); + + public static readonly StyledProperty GradientTypeProperty = + AvaloniaProperty.Register( + nameof(GradientType)); + + public static readonly StyledProperty NotifyableGradientProperty = + AvaloniaProperty.Register( + nameof(NotifyableGradient)); + + public NotifyableGradient NotifyableGradient + { + get => GetValue(NotifyableGradientProperty); + set => SetValue(NotifyableGradientProperty, value); + } + + public GradientType GradientType + { + get => GetValue(GradientTypeProperty); + set => SetValue(GradientTypeProperty, value); + } + + public int SelectedTabIndex + { + get => GetValue(SelectedTabIndexProperty); + set => SetValue(SelectedTabIndexProperty, value); + } + + public GradientBrush GradientBrush + { + get => GetValue(GradientBrushProperty); + set => SetValue(GradientBrushProperty, value); + } + + public GradientState GradientState + { + get => GetValue(GradientStateProperty); + set => SetValue(GradientStateProperty, value); + } + + public bool EnableGradientsTab + { + get => GetValue(EnableGradientsTabProperty); + set => SetValue(EnableGradientsTabProperty, value); + } + + private bool isUpdating; + + static DualColorGradientPickerBase() + { + GradientBrushProperty.Changed.Subscribe(OnGradientChanged); + SelectedTabIndexProperty.Changed.Subscribe(SelectedTabChanged); + GradientTypeProperty.Changed.Subscribe(GradientTypeChanged); + GradientStateProperty.Changed.Subscribe(GradientStateChanged); + } + + public DualColorGradientPickerBase() + { + ColorState stop0 = new(); + stop0.SetARGB(1, 0, 0, 0); + ColorState stop1 = new(); + stop1.SetARGB(1, 1, 1, 1); + + GradientState = new GradientState(new List + { + new GradientStop() { ColorState = stop0, Offset = 0 }, + new GradientStop() { ColorState = stop1, Offset = 1 } + }); + + GradientBrush = new LinearGradientBrush + { + GradientStops = new GradientStops + { + new Avalonia.Media.GradientStop(Colors.Black, 0), + new Avalonia.Media.GradientStop(Colors.White, 1) + } + }; + + NotifyableGradient = new NotifyableGradient(this); + } + + protected override void UpdateFromBrush(IBrush brush) + { + isUpdating = true; + if (brush is GradientBrush gradientBrush) + { + GradientType = gradientBrush switch + { + ILinearGradientBrush => GradientType.Linear, + IRadialGradientBrush => GradientType.Radial, + IConicGradientBrush => GradientType.Conic, + _ => throw new ArgumentOutOfRangeException() + }; + + var oldState = GradientState; + GradientState = StateFromBrush(gradientBrush); + NotifyableGradient?.UpdateEverything(oldState); + GradientBrush = gradientBrush; + SelectedTabIndex = EnableGradientsTab ? 1 : 0; + } + else + { + base.UpdateFromBrush(brush); + SelectedTabIndex = 0; + } + + isUpdating = false; + } + + protected override void UpdateSelectedBrush() + { + if (SelectedTabIndex == 1) + { + SelectedBrush = GradientBrush; + } + else + { + base.UpdateSelectedBrush(); + } + } + + private GradientState StateFromBrush(IGradientBrush brush) + { + List stops = new List(); + foreach (var stop in brush.GradientStops) + { + ColorState colorState = new ColorState(); + colorState.SetARGB(stop.Color.A / 255d, stop.Color.R / 255d, stop.Color.G / 255d, stop.Color.B / 255d); + stops.Add(new GradientStop { ColorState = colorState, Offset = stop.Offset }); + } + + return StateFromBrush(stops, brush, GradientState); + } + + private GradientState StateFromBrush(List stops, IGradientBrush brush, GradientState oldState) + { + if (brush is ILinearGradientBrush linearBrush) + { + return new GradientState(stops) + { + LinearStartPointX = linearBrush.StartPoint.Point.X, + LinearStartPointY = linearBrush.StartPoint.Point.Y, + LinearEndPointX = linearBrush.EndPoint.Point.X, + LinearEndPointY = linearBrush.EndPoint.Point.Y, + ConicAngle = oldState.ConicAngle, + ConicCenterX = oldState.ConicCenterX, + ConicCenterY = oldState.ConicCenterY, + RadialCenterX = oldState.RadialCenterX, + RadialCenterY = oldState.RadialCenterY, + RadialRadius = oldState.RadialRadius, + AbsoluteUnits = linearBrush.StartPoint.Unit == RelativeUnit.Absolute || + linearBrush.EndPoint.Unit == RelativeUnit.Absolute, + Transform = linearBrush.Transform != null ? ToMatrix(linearBrush.Transform.Value) : Matrix.Identity + }; + } + + if (brush is IRadialGradientBrush radialBrush) + { + return new GradientState(stops) + { + RadialCenterX = radialBrush.Center.Point.X, + RadialCenterY = radialBrush.Center.Point.Y, + RadialRadius = radialBrush.RadiusX.Scalar, + ConicAngle = oldState.ConicAngle, + ConicCenterX = oldState.ConicCenterX, + ConicCenterY = oldState.ConicCenterY, + LinearStartPointX = oldState.LinearStartPointX, + LinearStartPointY = oldState.LinearStartPointY, + LinearEndPointX = oldState.LinearEndPointX, + LinearEndPointY = oldState.LinearEndPointY, + AbsoluteUnits = radialBrush.Center.Unit == RelativeUnit.Absolute || + radialBrush.RadiusX.Unit == RelativeUnit.Absolute, + Transform = radialBrush.Transform != null ? ToMatrix(radialBrush.Transform.Value) : Matrix.Identity + }; + } + + if (brush is IConicGradientBrush conicBrush) + { + return new GradientState(stops) + { + ConicCenterX = conicBrush.Center.Point.X, + ConicCenterY = conicBrush.Center.Point.Y, + ConicAngle = conicBrush.Angle, + RadialCenterX = oldState.RadialCenterX, + RadialCenterY = oldState.RadialCenterY, + RadialRadius = oldState.RadialRadius, + LinearStartPointX = oldState.LinearStartPointX, + LinearStartPointY = oldState.LinearStartPointY, + LinearEndPointX = oldState.LinearEndPointX, + LinearEndPointY = oldState.LinearEndPointY, + AbsoluteUnits = conicBrush.Center.Unit == RelativeUnit.Absolute, + Transform = conicBrush.Transform != null ? ToMatrix(conicBrush.Transform.Value) : Matrix.Identity + }; + } + + return new GradientState(stops); + } + + private void UpdateGradientType() + { + var stops = GradientBrush?.GradientStops ?? new GradientStops(); + + UpdateGradientBrush(stops, RelativeUnit.Relative); + } + + private void UpdateGradientBrushFromState() + { + if (GradientState.Stops == null || GradientState.Stops.Count == 0) + { + GradientBrush = new LinearGradientBrush + { + GradientStops = new GradientStops + { + new Avalonia.Media.GradientStop(Colors.Black, 0), + new Avalonia.Media.GradientStop(Colors.White, 1) + } + }; + return; + } + + GradientStops stops = new GradientStops(); + foreach (var stop in GradientState.Stops) + { + stops.Add(new Avalonia.Media.GradientStop( + new Color( + (byte)(stop.ColorState.A * 255d), + (byte)(stop.ColorState.RGB_R * 255d), + (byte)(stop.ColorState.RGB_G * 255d), + (byte)(stop.ColorState.RGB_B * 255d)), stop.Offset)); + } + + UpdateGradientBrush(stops, + GradientState.AbsoluteUnits ? RelativeUnit.Absolute : RelativeUnit.Relative); + } + + private void UpdateGradientBrush(GradientStops stops, RelativeUnit unit) + { + GradientBrush = GradientType switch + { + GradientType.Linear => new LinearGradientBrush + { + GradientStops = stops, + StartPoint = + new RelativePoint(GradientState.LinearStartPointX, GradientState.LinearStartPointY, + unit), + EndPoint = new RelativePoint(GradientState.LinearEndPointX, GradientState.LinearEndPointY, + unit), + Transform = new MatrixTransform(ToAvMatrix(GradientState.Transform)) + }, + GradientType.Radial => new RadialGradientBrush + { + GradientStops = stops, + Center = + new RelativePoint(GradientState.RadialCenterX, GradientState.RadialCenterY, unit), + RadiusX = new RelativeScalar(GradientState.RadialRadius, RelativeUnit.Relative), + RadiusY = new RelativeScalar(GradientState.RadialRadius, RelativeUnit.Relative), + Transform = new MatrixTransform(ToAvMatrix(GradientState.Transform)) + }, + GradientType.Conic => new ConicGradientBrush + { + GradientStops = stops, + Center = new RelativePoint(GradientState.ConicCenterX, GradientState.ConicCenterY, + unit), + Angle = GradientState.ConicAngle, + Transform = new MatrixTransform(ToAvMatrix(GradientState.Transform)) + }, + _ => throw new ArgumentOutOfRangeException() + }; + } + + private static void OnGradientChanged(AvaloniaPropertyChangedEventArgs args) + { + if (args.Sender is DualColorGradientPickerBase picker) + { + if (picker.isUpdating) return; + + picker.UpdateSelectedBrush(); + } + } + + private static void SelectedTabChanged(AvaloniaPropertyChangedEventArgs args) + { + if (args.Sender is DualColorGradientPickerBase picker) + { + if (picker.isUpdating || picker.SelectedTabIndex == args.NewValue) return; + + picker.UpdateSelectedBrush(); + } + } + + private static void GradientTypeChanged(AvaloniaPropertyChangedEventArgs args) + { + if (args.Sender is DualColorGradientPickerBase picker) + { + if (picker.isUpdating) return; + + picker.UpdateGradientType(); + } + } + + private static void GradientStateChanged(AvaloniaPropertyChangedEventArgs args) + { + if (args.Sender is DualColorGradientPickerBase picker) + { + if (picker.isUpdating) return; + + picker.UpdateGradientBrushFromState(); + picker.UpdateSelectedBrush(); + picker.NotifyableGradient?.UpdateEverything(args.OldValue.Value); + } + } + + private static Avalonia.Matrix ToAvMatrix(Matrix matrix) + { + return new Avalonia.Matrix(matrix.ScaleX, matrix.SkewY, matrix.SkewX, matrix.ScaleY, matrix.TransX, + matrix.TransY); + } + + private static Matrix ToMatrix(Avalonia.Matrix matrix) + { + double scaleX = matrix.M11; + double skewY = matrix.M12; + double skewX = matrix.M21; + double scaleY = matrix.M22; + double offsetX = matrix.M31; + double offsetY = matrix.M32; + return new Matrix(scaleX, skewX, offsetX, skewY, scaleY, offsetY); + } +} diff --git a/src/ColorPicker.AvaloniaUI/GradientBar.cs b/src/ColorPicker.AvaloniaUI/GradientBar.cs new file mode 100644 index 0000000..74f8f85 --- /dev/null +++ b/src/ColorPicker.AvaloniaUI/GradientBar.cs @@ -0,0 +1,369 @@ +using System.Collections.ObjectModel; +using System.Collections.Specialized; +using System.Windows.Input; +using Avalonia; +using Avalonia.Controls; +using Avalonia.Controls.Metadata; +using Avalonia.Controls.Presenters; +using Avalonia.Controls.Primitives; +using Avalonia.Input; +using Avalonia.Interactivity; +using Avalonia.Media; +using ColorPicker.Models; +using ColorPicker.Utilities; +using GradientStop = ColorPicker.Models.GradientStop; + +namespace ColorPicker; + +[TemplatePart("PART_Bar", typeof(Border))] +[TemplatePart("PART_GradientStops", typeof(ItemsControl))] +[TemplatePart("PART_RemoveStopButton", typeof(Button))] +public class GradientBar : TemplatedControl, IGradientStorage, IColorStateStorage +{ + public static readonly StyledProperty GradientStateProperty = + AvaloniaProperty.Register( + nameof(GradientState)); + + public static readonly StyledProperty SelectedStopIndexProperty = AvaloniaProperty.Register( + nameof(SelectedStopIndex)); + + public static readonly StyledProperty SelectedStopProperty = + AvaloniaProperty.Register( + nameof(SelectedStop)); + + public static readonly StyledProperty SelectedStopStateProperty = + AvaloniaProperty.Register( + nameof(SelectedStopState)); + + public static readonly StyledProperty SelectedStopBindableProperty = + AvaloniaProperty.Register( + nameof(SelectedStopBindable)); + + public static readonly StyledProperty SelectedStopOffsetProperty = + AvaloniaProperty.Register( + nameof(SelectedStopOffset)); + + public double SelectedStopOffset + { + get => GetValue(SelectedStopOffsetProperty); + set => SetValue(SelectedStopOffsetProperty, value); + } + + public NotifyableColor SelectedStopBindable + { + get => GetValue(SelectedStopBindableProperty); + set => SetValue(SelectedStopBindableProperty, value); + } + + public static readonly StyledProperty GradientStopsProperty = + AvaloniaProperty.Register( + nameof(GradientStops)); + + public static readonly StyledProperty SelectColorStopCommandProperty = + AvaloniaProperty.Register( + nameof(SelectColorStopCommand)); + + public ICommand SelectColorStopCommand + { + get => GetValue(SelectColorStopCommandProperty); + set => SetValue(SelectColorStopCommandProperty, value); + } + + public GradientStops GradientStops + { + get => GetValue(GradientStopsProperty); + set => SetValue(GradientStopsProperty, value); + } + + public ColorState SelectedStopState + { + get => GetValue(SelectedStopStateProperty); + set => SetValue(SelectedStopStateProperty, value); + } + + public int SelectedStopIndex + { + get => GetValue(SelectedStopIndexProperty); + set => SetValue(SelectedStopIndexProperty, value); + } + + public GradientState GradientState + { + get => GetValue(GradientStateProperty); + set => SetValue(GradientStateProperty, value); + } + + public Avalonia.Media.GradientStop SelectedStop + { + get => GetValue(SelectedStopProperty); + set => SetValue(SelectedStopProperty, value); + } + + ColorState IColorStateStorage.ColorState + { + get => SelectedStopState; + set => SelectedStopState = value; + } + + private bool isUpdating; + + static GradientBar() + { + SelectedStopStateProperty.Changed.AddClassHandler(StopChanged); + SelectedStopOffsetProperty.Changed.AddClassHandler(StopOffsetChanged); + SelectedStopIndexProperty.Changed.AddClassHandler(IndexChanged); + GradientStateProperty.Changed.AddClassHandler(GradientStateChanged); + } + + private Border bar; + private ItemsControl stops; + private Button removeStopButton; + + private bool pauseStateUpdate; + + public GradientBar() + { + ColorState stop0 = new(); + stop0.SetARGB(1, 0, 0, 0); + ColorState stop1 = new(); + stop1.SetARGB(1, 1, 1, 1); + + GradientState = new GradientState(new List + { + new GradientStop() { ColorState = stop0, Offset = 0 }, + new GradientStop() { ColorState = stop1, Offset = 1 } + }); + + GradientStops = new GradientStops + { + new Avalonia.Media.GradientStop(ToColor(stop0), 0), new Avalonia.Media.GradientStop(ToColor(stop1), 1) + }; + + SelectedStopIndex = 0; + SelectedStopState = stop0; + SelectedStopOffset = 0; + + SelectColorStopCommand = new RelayCommand(stop => + { + int foundIndex = GradientStops.IndexOf(stop); + + if (foundIndex != -1) + { + SelectedStopIndex = foundIndex; + SelectedStopState = GradientState.Stops[foundIndex].ColorState; + SelectedStopOffset = GradientStops[foundIndex].Offset; + SelectedStop = GradientStops[foundIndex]; + } + }); + + SelectedStopBindable = new NotifyableColor(this); + } + + public void AddStop(double offset) + { + ColorState colorOnOffset = GradientState.Evaluate(offset); + GradientState newGradientState = GradientState.WithAddedStop(new GradientStop + { + ColorState = colorOnOffset, Offset = offset + }); + + UpdateInternalState(newGradientState); + } + + protected override void OnApplyTemplate(TemplateAppliedEventArgs e) + { + base.OnApplyTemplate(e); + bar = e.NameScope.Find("PART_Bar"); + bar.PointerPressed += BarOnPointerPressed; + bar.PointerMoved += BarMoved; + + stops = e.NameScope.Find("PART_GradientStops"); + + removeStopButton = e.NameScope.Find