From 435adc6e81fd38a47d732adc13bcc008ffa01f8a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Krzysztof=20Krysi=C5=84ski?= Date: Mon, 3 Mar 2025 13:50:57 +0100 Subject: [PATCH 01/30] gradients wip --- .../ColorPickerDemo.AvaloniaUI.Browser.csproj | 2 +- .../ColorPickerDemo.AvaloniaUI.Desktop.csproj | 2 +- .../ColorPickerDemo.AvaloniaUI.csproj | 2 +- .../Directory.Build.props | 2 +- .../ColorPickerDemo/ColorPickerDemo.csproj | 2 +- .../ColorPicker.AvaloniaUI.csproj | 11 +- .../Converters/OffsetToMarginConverter.cs | 27 ++++ src/ColorPicker.AvaloniaUI/GradientBar.cs | 138 ++++++++++++++++++ .../Templates/ColorPicker.Templates.axaml | 1 + .../Templates/GradientBar.axaml | 72 +++++++++ .../Templates/StandardColorPicker.axaml | 117 +++++++++------ .../Utilities/RelayCommand.cs | 27 ++++ src/ColorPicker.Models/GradientState.cs | 20 +++ src/ColorPicker.Models/IGradientStorage.cs | 7 + src/ColorPicker/ColorPicker.csproj | 2 +- 15 files changed, 376 insertions(+), 56 deletions(-) create mode 100644 src/ColorPicker.AvaloniaUI/Converters/OffsetToMarginConverter.cs create mode 100644 src/ColorPicker.AvaloniaUI/GradientBar.cs create mode 100644 src/ColorPicker.AvaloniaUI/Templates/GradientBar.axaml create mode 100644 src/ColorPicker.AvaloniaUI/Utilities/RelayCommand.cs create mode 100644 src/ColorPicker.Models/GradientState.cs create mode 100644 src/ColorPicker.Models/IGradientStorage.cs diff --git a/ColorPickerDemo/ColorPickerDemo.AvaloniaUI/ColorPickerDemo.AvaloniaUI.Browser/ColorPickerDemo.AvaloniaUI.Browser.csproj b/ColorPickerDemo/ColorPickerDemo.AvaloniaUI/ColorPickerDemo.AvaloniaUI.Browser/ColorPickerDemo.AvaloniaUI.Browser.csproj index 53b12a2..9f5a765 100644 --- a/ColorPickerDemo/ColorPickerDemo.AvaloniaUI/ColorPickerDemo.AvaloniaUI.Browser/ColorPickerDemo.AvaloniaUI.Browser.csproj +++ b/ColorPickerDemo/ColorPickerDemo.AvaloniaUI/ColorPickerDemo.AvaloniaUI.Browser/ColorPickerDemo.AvaloniaUI.Browser.csproj @@ -1,6 +1,6 @@  - net7.0 + net8.0 browser-wasm AppBundle\main.js Exe diff --git a/ColorPickerDemo/ColorPickerDemo.AvaloniaUI/ColorPickerDemo.AvaloniaUI.Desktop/ColorPickerDemo.AvaloniaUI.Desktop.csproj b/ColorPickerDemo/ColorPickerDemo.AvaloniaUI/ColorPickerDemo.AvaloniaUI.Desktop/ColorPickerDemo.AvaloniaUI.Desktop.csproj index a1775da..9977799 100644 --- a/ColorPickerDemo/ColorPickerDemo.AvaloniaUI/ColorPickerDemo.AvaloniaUI.Desktop/ColorPickerDemo.AvaloniaUI.Desktop.csproj +++ b/ColorPickerDemo/ColorPickerDemo.AvaloniaUI/ColorPickerDemo.AvaloniaUI.Desktop/ColorPickerDemo.AvaloniaUI.Desktop.csproj @@ -3,7 +3,7 @@ WinExe - net7.0 + net8.0 enable true diff --git a/ColorPickerDemo/ColorPickerDemo.AvaloniaUI/ColorPickerDemo.AvaloniaUI/ColorPickerDemo.AvaloniaUI.csproj b/ColorPickerDemo/ColorPickerDemo.AvaloniaUI/ColorPickerDemo.AvaloniaUI/ColorPickerDemo.AvaloniaUI.csproj index c3504ee..f9ceefc 100644 --- a/ColorPickerDemo/ColorPickerDemo.AvaloniaUI/ColorPickerDemo.AvaloniaUI/ColorPickerDemo.AvaloniaUI.csproj +++ b/ColorPickerDemo/ColorPickerDemo.AvaloniaUI/ColorPickerDemo.AvaloniaUI/ColorPickerDemo.AvaloniaUI.csproj @@ -1,6 +1,6 @@  - net7.0 + net8.0 enable latest true diff --git a/ColorPickerDemo/ColorPickerDemo.AvaloniaUI/Directory.Build.props b/ColorPickerDemo/ColorPickerDemo.AvaloniaUI/Directory.Build.props index d25ac51..10ab2cd 100644 --- a/ColorPickerDemo/ColorPickerDemo.AvaloniaUI/Directory.Build.props +++ b/ColorPickerDemo/ColorPickerDemo.AvaloniaUI/Directory.Build.props @@ -1,6 +1,6 @@ enable - 11.0.5 + 11.2.5 diff --git a/ColorPickerDemo/ColorPickerDemo/ColorPickerDemo.csproj b/ColorPickerDemo/ColorPickerDemo/ColorPickerDemo.csproj index 8ca24f1..31ee3de 100644 --- a/ColorPickerDemo/ColorPickerDemo/ColorPickerDemo.csproj +++ b/ColorPickerDemo/ColorPickerDemo/ColorPickerDemo.csproj @@ -2,7 +2,7 @@ WinExe - net7.0-windows + net8.0-windows true diff --git a/src/ColorPicker.AvaloniaUI/ColorPicker.AvaloniaUI.csproj b/src/ColorPicker.AvaloniaUI/ColorPicker.AvaloniaUI.csproj index 2ef5eb8..447618e 100644 --- a/src/ColorPicker.AvaloniaUI/ColorPicker.AvaloniaUI.csproj +++ b/src/ColorPicker.AvaloniaUI/ColorPicker.AvaloniaUI.csproj @@ -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/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/GradientBar.cs b/src/ColorPicker.AvaloniaUI/GradientBar.cs new file mode 100644 index 0000000..7bfcbca --- /dev/null +++ b/src/ColorPicker.AvaloniaUI/GradientBar.cs @@ -0,0 +1,138 @@ +using System.Collections.ObjectModel; +using System.Windows.Input; +using Avalonia; +using Avalonia.Controls; +using Avalonia.Controls.Metadata; +using Avalonia.Controls.Primitives; +using Avalonia.Media; +using ColorPicker.Models; +using ColorPicker.Utilities; +using GradientStop = ColorPicker.Models.GradientStop; + +namespace ColorPicker; + +public class GradientBar : TemplatedControl, IGradientStorage +{ + public static readonly StyledProperty GradientStateProperty = AvaloniaProperty.Register( + nameof(GradientState)); + + public static readonly StyledProperty BrushProperty = AvaloniaProperty.Register( + nameof(Brush)); + + public static readonly StyledProperty SelectedStopIndexProperty = AvaloniaProperty.Register( + nameof(SelectedStopIndex)); + + public static readonly StyledProperty SelectedStopStateProperty = AvaloniaProperty.Register( + nameof(SelectedStopState)); + + 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 ObservableCollection 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 GradientBrush Brush + { + get => GetValue(BrushProperty); + set => SetValue(BrushProperty, value); + } + + public GradientState GradientState + { + get => GetValue(GradientStateProperty); + set => SetValue(GradientStateProperty, value); + } + + static GradientBar() + { + SelectedStopStateProperty.Changed.AddClassHandler(StopChanged); + } + + 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 ObservableCollection(new[] + { + new Avalonia.Media.GradientStop(ToColor(stop0), 0), + new Avalonia.Media.GradientStop(ToColor(stop1), 1) + }); + + GenerateBrush(); + + SelectedStopIndex = 0; + SelectedStopState = stop0; + + SelectColorStopCommand = new RelayCommand(stop => + { + int foundIndex = GradientStops.IndexOf(stop); + if (foundIndex != -1) + { + SelectedStopIndex = foundIndex; + SelectedStopState = GradientState.GradientStops[SelectedStopIndex].ColorState; + } + }); + } + + private void GenerateBrush() + { + GradientStops stops = new GradientStops(); + foreach (var stop in GradientState.GradientStops) + { + stops.Add(new Avalonia.Media.GradientStop(ToColor(stop.ColorState), stop.Offset)); + } + + Brush = new LinearGradientBrush() + { + GradientStops = stops + }; + } + + private static Color ToColor(ColorState colorState) + { + return Color.FromArgb((byte)(colorState.A * 255), (byte)(colorState.RGB_R * 255), (byte)(colorState.RGB_G * 255), (byte)(colorState.RGB_B * 255)); + } + + private static void StopChanged(GradientBar sender, AvaloniaPropertyChangedEventArgs e) + { + sender.GradientState.GradientStops[sender.SelectedStopIndex] = new GradientStop() + { + ColorState = e.NewValue.Value, Offset = sender.GradientState.GradientStops[sender.SelectedStopIndex].Offset + }; + + sender.GenerateBrush(); + } +} \ No newline at end of file diff --git a/src/ColorPicker.AvaloniaUI/Templates/ColorPicker.Templates.axaml b/src/ColorPicker.AvaloniaUI/Templates/ColorPicker.Templates.axaml index c39be0a..92a98cc 100644 --- a/src/ColorPicker.AvaloniaUI/Templates/ColorPicker.Templates.axaml +++ b/src/ColorPicker.AvaloniaUI/Templates/ColorPicker.Templates.axaml @@ -10,5 +10,6 @@ + diff --git a/src/ColorPicker.AvaloniaUI/Templates/GradientBar.axaml b/src/ColorPicker.AvaloniaUI/Templates/GradientBar.axaml new file mode 100644 index 0000000..2e7613f --- /dev/null +++ b/src/ColorPicker.AvaloniaUI/Templates/GradientBar.axaml @@ -0,0 +1,72 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + 15 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/ColorPicker.AvaloniaUI/Templates/StandardColorPicker.axaml b/src/ColorPicker.AvaloniaUI/Templates/StandardColorPicker.axaml index f7a3ca5..aa75cf1 100644 --- a/src/ColorPicker.AvaloniaUI/Templates/StandardColorPicker.axaml +++ b/src/ColorPicker.AvaloniaUI/Templates/StandardColorPicker.axaml @@ -5,56 +5,77 @@ - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + - - HSV - HSL - + + HSV + HSL + - - - - - - + + + + + + + + + + + + + + + diff --git a/src/ColorPicker.AvaloniaUI/Utilities/RelayCommand.cs b/src/ColorPicker.AvaloniaUI/Utilities/RelayCommand.cs new file mode 100644 index 0000000..a2dfce2 --- /dev/null +++ b/src/ColorPicker.AvaloniaUI/Utilities/RelayCommand.cs @@ -0,0 +1,27 @@ +using System.Windows.Input; + +namespace ColorPicker.Utilities; + +internal class RelayCommand : ICommand +{ + private Action execute; + private Func canExecute; + + public RelayCommand(Action execute, Func canExecute = null) + { + this.execute = execute; + this.canExecute = canExecute; + } + + public bool CanExecute(object parameter) + { + return canExecute == null || canExecute((T)parameter); + } + + public void Execute(object parameter) + { + execute((T)parameter); + } + + public event EventHandler CanExecuteChanged; +} \ No newline at end of file diff --git a/src/ColorPicker.Models/GradientState.cs b/src/ColorPicker.Models/GradientState.cs new file mode 100644 index 0000000..e3f486f --- /dev/null +++ b/src/ColorPicker.Models/GradientState.cs @@ -0,0 +1,20 @@ +using System.Collections.Generic; + +namespace ColorPicker.Models +{ + public class GradientState + { + public List GradientStops { get; set; } + + public GradientState(List gradientStops) + { + GradientStops = gradientStops; + } + } + + public struct GradientStop + { + public ColorState ColorState { get; set; } + public double Offset { get; set; } + } +} \ No newline at end of file diff --git a/src/ColorPicker.Models/IGradientStorage.cs b/src/ColorPicker.Models/IGradientStorage.cs new file mode 100644 index 0000000..41e9dd0 --- /dev/null +++ b/src/ColorPicker.Models/IGradientStorage.cs @@ -0,0 +1,7 @@ +namespace ColorPicker.Models +{ + public interface IGradientStorage + { + GradientState GradientState { get; set; } + } +} \ No newline at end of file diff --git a/src/ColorPicker/ColorPicker.csproj b/src/ColorPicker/ColorPicker.csproj index eba4975..1f72fd2 100644 --- a/src/ColorPicker/ColorPicker.csproj +++ b/src/ColorPicker/ColorPicker.csproj @@ -1,7 +1,7 @@  - net451;netcoreapp3.1;net5.0-windows;net6.0-windows;net7.0-windows + net451;netcoreapp3.1;net5.0-windows;net6.0-windows;net7.0-windows;net8.0-windows true Egor Mozgovoy, Krzysztof Krysiński PixiEditor From 352299fdeec9c4f4fc2c04cd25bfb80f3ff35578 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Krzysztof=20Krysi=C5=84ski?= Date: Mon, 3 Mar 2025 17:58:43 +0100 Subject: [PATCH 02/30] Basic gradient bar works --- .../Converters/EqualsConverter.cs | 17 ++ src/ColorPicker.AvaloniaUI/GradientBar.cs | 164 ++++++++++++++++-- .../Styles/PixiPerfectColors.axaml | 2 + .../Templates/GradientBar.axaml | 82 ++++++--- src/ColorPicker.Models/ColorState.cs | 34 +++- src/ColorPicker.Models/GradientState.cs | 40 ++++- 6 files changed, 290 insertions(+), 49 deletions(-) create mode 100644 src/ColorPicker.AvaloniaUI/Converters/EqualsConverter.cs 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/GradientBar.cs b/src/ColorPicker.AvaloniaUI/GradientBar.cs index 7bfcbca..07f42d9 100644 --- a/src/ColorPicker.AvaloniaUI/GradientBar.cs +++ b/src/ColorPicker.AvaloniaUI/GradientBar.cs @@ -1,9 +1,12 @@ 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.Media; using ColorPicker.Models; using ColorPicker.Utilities; @@ -11,25 +14,36 @@ namespace ColorPicker; +[TemplatePart("PART_Bar", typeof(Border))] +[TemplatePart("PART_GradientStops", typeof(ItemsControl))] public class GradientBar : TemplatedControl, IGradientStorage { - public static readonly StyledProperty GradientStateProperty = AvaloniaProperty.Register( - nameof(GradientState)); + public static readonly StyledProperty GradientStateProperty = + AvaloniaProperty.Register( + nameof(GradientState)); - public static readonly StyledProperty BrushProperty = AvaloniaProperty.Register( - nameof(Brush)); + public static readonly StyledProperty BrushProperty = + AvaloniaProperty.Register( + nameof(Brush)); public static readonly StyledProperty SelectedStopIndexProperty = AvaloniaProperty.Register( nameof(SelectedStopIndex)); - public static readonly StyledProperty SelectedStopStateProperty = AvaloniaProperty.Register( - nameof(SelectedStopState)); + public static readonly StyledProperty SelectedStopProperty = + AvaloniaProperty.Register( + nameof(SelectedStop)); - public static readonly StyledProperty> GradientStopsProperty = AvaloniaProperty.Register>( - nameof(GradientStops)); + public static readonly StyledProperty SelectedStopStateProperty = + AvaloniaProperty.Register( + nameof(SelectedStopState)); - public static readonly StyledProperty SelectColorStopCommandProperty = AvaloniaProperty.Register( - nameof(SelectColorStopCommand)); + public static readonly StyledProperty> GradientStopsProperty = + AvaloniaProperty.Register>( + nameof(GradientStops)); + + public static readonly StyledProperty SelectColorStopCommandProperty = + AvaloniaProperty.Register( + nameof(SelectColorStopCommand)); public ICommand SelectColorStopCommand { @@ -67,11 +81,22 @@ public GradientState GradientState set => SetValue(GradientStateProperty, value); } + public Avalonia.Media.GradientStop SelectedStop + { + get => GetValue(SelectedStopProperty); + set => SetValue(SelectedStopProperty, value); + } + static GradientBar() { SelectedStopStateProperty.Changed.AddClassHandler(StopChanged); + SelectedStopIndexProperty.Changed.AddClassHandler(IndexChanged); + GradientStateProperty.Changed.AddClassHandler(GradientStateChanged); } + private Border bar; + private ItemsControl stops; + public GradientBar() { ColorState stop0 = new(); @@ -99,20 +124,89 @@ public GradientBar() SelectColorStopCommand = new RelayCommand(stop => { int foundIndex = GradientStops.IndexOf(stop); + if (foundIndex != -1) { SelectedStopIndex = foundIndex; - SelectedStopState = GradientState.GradientStops[SelectedStopIndex].ColorState; + SelectedStopState = GradientState.Stops[foundIndex].ColorState; + SelectedStop = GradientStops[foundIndex]; } }); } + 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"); + } + + private void BarMoved(object sender, PointerEventArgs e) + { + if (e.Pointer.Captured != null && e.Pointer.Captured.Equals(bar)) + { + if (e.GetCurrentPoint(bar).Properties.IsLeftButtonPressed) + { + double offset = GetNormalizedOffset(e); + + GradientState newGradientState = GradientState.WithUpdatedStop(SelectedStopIndex, + new GradientStop + { ColorState = GradientState.Stops[SelectedStopIndex].ColorState, Offset = offset }); + + UpdateInternalState(newGradientState); + } + } + } + + private void BarOnPointerPressed(object sender, PointerPressedEventArgs e) + { + double normalizedClosestOffset = GetNormalizedOffset(e); + + int closestStopIndex = 0; + double closestDistance = double.MaxValue; + + for (int i = 0; i < GradientState.Stops.Count; i++) + { + double distance = Math.Abs(GradientState.Stops[i].Offset - normalizedClosestOffset); + if (distance < closestDistance) + { + closestDistance = distance; + closestStopIndex = i; + } + } + + SelectedStopIndex = closestStopIndex; + + e.Pointer.Capture(bar); + + if (e.Source != null && e.Source.Equals(stops.ItemsPanelRoot)) + { + double offset = GetNormalizedOffset(e); + + GradientState newGradientState = GradientState.WithUpdatedStop(SelectedStopIndex, + new GradientStop + { ColorState = GradientState.Stops[SelectedStopIndex].ColorState, Offset = offset }); + + UpdateInternalState(newGradientState); + } + } + + private double GetNormalizedOffset(PointerEventArgs e) + { + return Math.Clamp( + (e.GetPosition(bar).X - bar.Padding.Left) / (bar.Bounds.Width - (bar.Padding.Left + bar.Padding.Right)), 0, + 1); + } + private void GenerateBrush() { GradientStops stops = new GradientStops(); - foreach (var stop in GradientState.GradientStops) + foreach (var stop in GradientStops) { - stops.Add(new Avalonia.Media.GradientStop(ToColor(stop.ColorState), stop.Offset)); + stops.Add(stop); } Brush = new LinearGradientBrush() @@ -123,16 +217,52 @@ private void GenerateBrush() private static Color ToColor(ColorState colorState) { - return Color.FromArgb((byte)(colorState.A * 255), (byte)(colorState.RGB_R * 255), (byte)(colorState.RGB_G * 255), (byte)(colorState.RGB_B * 255)); + return Color.FromArgb((byte)(colorState.A * 255), (byte)(colorState.RGB_R * 255), + (byte)(colorState.RGB_G * 255), (byte)(colorState.RGB_B * 255)); + } + + private void UpdateInternalState(GradientState newGradientState) + { + GradientState = newGradientState; + + if (GradientStops == null) + { + GradientStops = new ObservableCollection(); + } + else + { + GradientStops.Clear(); + } + + foreach (var stop in GradientState.Stops) + { + GradientStops.Add(new Avalonia.Media.GradientStop(ToColor(stop.ColorState), stop.Offset)); + } + + SelectedStopState = GradientState.Stops[SelectedStopIndex].ColorState; + SelectedStop = GradientStops[SelectedStopIndex]; + + GenerateBrush(); } private static void StopChanged(GradientBar sender, AvaloniaPropertyChangedEventArgs e) { - sender.GradientState.GradientStops[sender.SelectedStopIndex] = new GradientStop() + var newStop = new GradientStop { - ColorState = e.NewValue.Value, Offset = sender.GradientState.GradientStops[sender.SelectedStopIndex].Offset + ColorState = e.NewValue.Value, Offset = sender.GradientState.Stops[sender.SelectedStopIndex].Offset }; - sender.GenerateBrush(); + sender.UpdateInternalState(sender.GradientState.WithUpdatedStop(sender.SelectedStopIndex, newStop)); + } + + private static void IndexChanged(GradientBar sender, AvaloniaPropertyChangedEventArgs e) + { + sender.SelectedStopState = sender.GradientState.Stops[e.NewValue.Value].ColorState; + sender.SelectedStop = sender.GradientStops[e.NewValue.Value]; + } + + private static void GradientStateChanged(GradientBar sender, AvaloniaPropertyChangedEventArgs e) + { + sender.UpdateInternalState(e.NewValue.Value); } } \ No newline at end of file diff --git a/src/ColorPicker.AvaloniaUI/Styles/PixiPerfectColors.axaml b/src/ColorPicker.AvaloniaUI/Styles/PixiPerfectColors.axaml index da88a86..db93708 100644 --- a/src/ColorPicker.AvaloniaUI/Styles/PixiPerfectColors.axaml +++ b/src/ColorPicker.AvaloniaUI/Styles/PixiPerfectColors.axaml @@ -7,6 +7,7 @@ #4F4F4F #999 #2d2d2d + #5fad65 @@ -25,4 +26,5 @@ + diff --git a/src/ColorPicker.AvaloniaUI/Templates/GradientBar.axaml b/src/ColorPicker.AvaloniaUI/Templates/GradientBar.axaml index 2e7613f..f54d935 100644 --- a/src/ColorPicker.AvaloniaUI/Templates/GradientBar.axaml +++ b/src/ColorPicker.AvaloniaUI/Templates/GradientBar.axaml @@ -2,7 +2,8 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:colorPicker="clr-namespace:ColorPicker" xmlns:converters="clr-namespace:ColorPicker.Converters" - xmlns:system="clr-namespace:System;assembly=System.Runtime"> + xmlns:system="clr-namespace:System;assembly=System.Runtime" + xmlns:models="clr-namespace:ColorPicker.Models;assembly=ColorPicker.Models"> @@ -11,13 +12,29 @@ + - - + + - + + + + + + + + + + + + @@ -27,39 +44,54 @@ - + + - - - - - + + + + + + + + + + - - - - - - - - + + + + + + + + + + + diff --git a/src/ColorPicker.Models/ColorState.cs b/src/ColorPicker.Models/ColorState.cs index 77ed4fe..710fc01 100644 --- a/src/ColorPicker.Models/ColorState.cs +++ b/src/ColorPicker.Models/ColorState.cs @@ -1,6 +1,8 @@ -namespace ColorPicker.Models +using System; + +namespace ColorPicker.Models { - public struct ColorState + public struct ColorState : IEquatable { private double _RGB_R; private double _RGB_G; @@ -197,5 +199,33 @@ private void RecalculateRGBFromHSV() _RGB_G = rgbtuple.Item2; _RGB_B = rgbtuple.Item3; } + + public bool Equals(ColorState other) + { + return _RGB_R.Equals(other._RGB_R) && _RGB_G.Equals(other._RGB_G) && _RGB_B.Equals(other._RGB_B) && _HSV_H.Equals(other._HSV_H) && _HSV_S.Equals(other._HSV_S) && _HSV_V.Equals(other._HSV_V) && _HSL_H.Equals(other._HSL_H) && _HSL_S.Equals(other._HSL_S) && _HSL_L.Equals(other._HSL_L) && A.Equals(other.A); + } + + public override bool Equals(object obj) + { + return obj is ColorState other && Equals(other); + } + + public override int GetHashCode() + { + unchecked + { + var hashCode = _RGB_R.GetHashCode(); + hashCode = (hashCode * 397) ^ _RGB_G.GetHashCode(); + hashCode = (hashCode * 397) ^ _RGB_B.GetHashCode(); + hashCode = (hashCode * 397) ^ _HSV_H.GetHashCode(); + hashCode = (hashCode * 397) ^ _HSV_S.GetHashCode(); + hashCode = (hashCode * 397) ^ _HSV_V.GetHashCode(); + hashCode = (hashCode * 397) ^ _HSL_H.GetHashCode(); + hashCode = (hashCode * 397) ^ _HSL_S.GetHashCode(); + hashCode = (hashCode * 397) ^ _HSL_L.GetHashCode(); + hashCode = (hashCode * 397) ^ A.GetHashCode(); + return hashCode; + } + } } } \ No newline at end of file diff --git a/src/ColorPicker.Models/GradientState.cs b/src/ColorPicker.Models/GradientState.cs index e3f486f..71cba3e 100644 --- a/src/ColorPicker.Models/GradientState.cs +++ b/src/ColorPicker.Models/GradientState.cs @@ -1,20 +1,50 @@ +using System; using System.Collections.Generic; namespace ColorPicker.Models { - public class GradientState + public struct GradientState { - public List GradientStops { get; set; } + private List stops; + public IReadOnlyList Stops + { + get => stops; + } - public GradientState(List gradientStops) + public GradientState(List stops) { - GradientStops = gradientStops; + this.stops = stops; + } + + public GradientState WithUpdatedStop(int stopIndex, GradientStop newStop) + { + GradientState newStopState = new GradientState(stops); + newStopState.stops[stopIndex] = newStop; + return newStopState; } } - public struct GradientStop + public struct GradientStop : IEquatable { public ColorState ColorState { get; set; } public double Offset { get; set; } + + public bool Equals(GradientStop other) + { + return ColorState.Equals(other.ColorState) && Offset.Equals(other.Offset); + } + + public override bool Equals(object obj) + { + return obj is GradientStop other && Equals(other); + } + + public override int GetHashCode() + { + unchecked + { + return (ColorState.GetHashCode() * 397) ^ Offset.GetHashCode(); + } + } } } \ No newline at end of file From 1cb2443cd7989246428dd68bae6df50cc745ec2f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Krzysztof=20Krysi=C5=84ski?= Date: Tue, 4 Mar 2025 12:59:52 +0100 Subject: [PATCH 03/30] Exposed gradient related properties --- src/ColorPicker.AvaloniaUI/GradientBar.cs | 53 +++++++- .../PickerControlBase.cs | 20 ++- .../PortableColorPicker.cs | 45 ++++++- .../StandardColorPicker.cs | 126 +++++++++++++++++- .../Styles/PixiPerfectTabControlStyle.axaml | 4 +- .../Templates/GradientBar.axaml | 16 +-- .../Templates/PortableColorPicker.axaml | 7 +- .../Templates/StandardColorPicker.axaml | 13 +- src/ColorPicker.Models/ColorState.cs | 21 ++- src/ColorPicker.Models/GradientState.cs | 45 +++++++ 10 files changed, 320 insertions(+), 30 deletions(-) diff --git a/src/ColorPicker.AvaloniaUI/GradientBar.cs b/src/ColorPicker.AvaloniaUI/GradientBar.cs index 07f42d9..613c765 100644 --- a/src/ColorPicker.AvaloniaUI/GradientBar.cs +++ b/src/ColorPicker.AvaloniaUI/GradientBar.cs @@ -7,6 +7,7 @@ using Avalonia.Controls.Presenters; using Avalonia.Controls.Primitives; using Avalonia.Input; +using Avalonia.Interactivity; using Avalonia.Media; using ColorPicker.Models; using ColorPicker.Utilities; @@ -16,6 +17,7 @@ namespace ColorPicker; [TemplatePart("PART_Bar", typeof(Border))] [TemplatePart("PART_GradientStops", typeof(ItemsControl))] +[TemplatePart("PART_RemoveStopButton", typeof(Button))] public class GradientBar : TemplatedControl, IGradientStorage { public static readonly StyledProperty GradientStateProperty = @@ -96,6 +98,7 @@ static GradientBar() private Border bar; private ItemsControl stops; + private Button removeStopButton; public GradientBar() { @@ -134,6 +137,18 @@ public GradientBar() }); } + 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); @@ -142,6 +157,12 @@ protected override void OnApplyTemplate(TemplateAppliedEventArgs e) bar.PointerMoved += BarMoved; stops = e.NameScope.Find("PART_GradientStops"); + + removeStopButton = e.NameScope.Find