From 65e5e580acd359c0103e7b9bb3b0397d7676f1e1 Mon Sep 17 00:00:00 2001 From: robloo Date: Tue, 26 Apr 2022 21:57:19 -0400 Subject: [PATCH] Make ColorSlider fully functional --- .../ControlCatalog/Pages/ColorPickerPage.xaml | 3 +- .../ColorSlider/ColorSlider.cs | 367 ++++++++++++------ .../Helpers/ColorHelpers.cs | 2 +- 3 files changed, 241 insertions(+), 131 deletions(-) diff --git a/samples/ControlCatalog/Pages/ColorPickerPage.xaml b/samples/ControlCatalog/Pages/ColorPickerPage.xaml index f343fd8f59..09ec15ad15 100644 --- a/samples/ControlCatalog/Pages/ColorPickerPage.xaml +++ b/samples/ControlCatalog/Pages/ColorPickerPage.xaml @@ -25,7 +25,8 @@ Width="256" /> + RowDefinitions="Auto,Auto,Auto,Auto,Auto" + Margin="0,10,0,0"> public partial class ColorSlider : Slider { - private Size cachedSize = Size.Empty; + private bool disableUpdates = false; /// /// Initializes a new instance of the class. @@ -19,200 +20,308 @@ namespace Avalonia.Controls.Primitives } /// - /// Update the slider's Foreground and Background brushes based on the current slider state and color. + /// Generates a new background image for the color slider and applies it. /// - /// - /// Manually refreshes the background gradient of the slider. - /// This is callable separately for performance reasons. - /// - public void UpdateColors() + private async void UpdateBackground() { - HsvColor hsvColor = HsvColor; + // In Avalonia, Bounds returns the actual device-independent pixel size of a control. + // However, this is not necessarily the size of the control rendered on a display. + // A desktop or application scaling factor may be applied which must be accounted for here. + // Remember bitmaps in Avalonia are rendered mapping to actual device pixels, not the device- + // independent pixels of controls. - // Calculate and set the background - UpdateBackground(hsvColor); + var scale = LayoutHelper.GetLayoutScale(this); + var pixelWidth = Convert.ToInt32(Bounds.Width * scale); + var pixelHeight = Convert.ToInt32(Bounds.Height * scale); - // Calculate and set the foreground ensuring contrast with the background - Color rgbColor = hsvColor.ToRgb(); - Color selectedRgbColor; - double sliderPercent = Value / (Maximum - Minimum); + if (pixelWidth != 0 && pixelHeight != 0) + { + var bitmap = await ColorHelpers.CreateComponentBitmapAsync( + pixelWidth, + pixelHeight, + Orientation, + ColorModel, + ColorComponent, + HsvColor, + IsAlphaMaxForced, + IsSaturationValueMaxForced); + + if (bitmap != null) + { + Background = ColorHelpers.BitmapToBrushAsync(bitmap, pixelWidth, pixelHeight); + } + } + + return; + } + /// + /// Updates the slider property values by applying the current color. + /// + /// + /// Warning: This will trigger property changed updates. + /// Consider using externally. + /// + private void SetColorToSliderValues() + { + var hsvColor = HsvColor; + var rgbColor = Color; var component = ColorComponent; if (ColorModel == ColorModel.Hsva) { - if (IsAlphaMaxForced && - component != ColorComponent.Alpha) + // Note: Components converted into a usable range for the user + switch (component) { - hsvColor = new HsvColor(1.0, hsvColor.H, hsvColor.S, hsvColor.V); + case ColorComponent.Alpha: + Minimum = 0; + Maximum = 100; + Value = hsvColor.A * 100; + break; + case ColorComponent.Component1: // Hue + Minimum = 0; + Maximum = 359; + Value = hsvColor.H; + break; + case ColorComponent.Component2: // Saturation + Minimum = 0; + Maximum = 100; + Value = hsvColor.S * 100; + break; + case ColorComponent.Component3: // Value + Minimum = 0; + Maximum = 100; + Value = hsvColor.V * 100; + break; } - + } + else + { switch (component) { - case ColorComponent.Component1: - { - var componentValue = MathUtilities.Clamp(sliderPercent * 360.0, 0.0, 360.0); - - hsvColor = new HsvColor( - hsvColor.A, - componentValue, - IsSaturationValueMaxForced ? 1.0 : hsvColor.S, - IsSaturationValueMaxForced ? 1.0 : hsvColor.V); - - break; - } + case ColorComponent.Alpha: + Minimum = 0; + Maximum = 255; + Value = Convert.ToDouble(rgbColor.A); + break; + case ColorComponent.Component1: // Red + Minimum = 0; + Maximum = 255; + Value = Convert.ToDouble(rgbColor.R); + break; + case ColorComponent.Component2: // Green + Minimum = 0; + Maximum = 255; + Value = Convert.ToDouble(rgbColor.G); + break; + case ColorComponent.Component3: // Blue + Minimum = 0; + Maximum = 255; + Value = Convert.ToDouble(rgbColor.B); + break; + } + } - case ColorComponent.Component2: - { - var componentValue = MathUtilities.Clamp(sliderPercent * 1.0, 0.0, 1.0); + return; + } - hsvColor = new HsvColor( - hsvColor.A, - hsvColor.H, - componentValue, - IsSaturationValueMaxForced ? 1.0 : hsvColor.V); + /// + /// Gets the current color determined by the slider values. + /// + private (Color, HsvColor) GetColorFromSliderValues() + { + HsvColor hsvColor = new HsvColor(); + Color rgbColor = new Color(); + double sliderPercent = Value / (Maximum - Minimum); - break; - } + var baseHsvColor = HsvColor; + var baseRgbColor = Color; + var component = ColorComponent; + if (ColorModel == ColorModel.Hsva) + { + switch (component) + { + case ColorComponent.Alpha: + { + var componentValue = MathUtilities.Clamp(sliderPercent * 1.0, 0.0, 1.0); + hsvColor = new HsvColor(componentValue, baseHsvColor.H, baseHsvColor.S, baseHsvColor.V); + break; + } + case ColorComponent.Component1: + { + var componentValue = MathUtilities.Clamp(sliderPercent * 360.0, 0.0, 360.0); + hsvColor = new HsvColor(baseHsvColor.A, componentValue, baseHsvColor.S, baseHsvColor.V); + break; + } + case ColorComponent.Component2: + { + var componentValue = MathUtilities.Clamp(sliderPercent * 1.0, 0.0, 1.0); + hsvColor = new HsvColor(baseHsvColor.A, baseHsvColor.H, componentValue, baseHsvColor.V); + break; + } case ColorComponent.Component3: - { - var componentValue = MathUtilities.Clamp(sliderPercent * 1.0, 0.0, 1.0); - - hsvColor = new HsvColor( - hsvColor.A, - hsvColor.H, - IsSaturationValueMaxForced ? 1.0 : hsvColor.S, - componentValue); - - break; - } + { + var componentValue = MathUtilities.Clamp(sliderPercent * 1.0, 0.0, 1.0); + hsvColor = new HsvColor(baseHsvColor.A, baseHsvColor.H, baseHsvColor.S, componentValue); + break; + } } - selectedRgbColor = hsvColor.ToRgb(); + return (hsvColor.ToRgb(), hsvColor); } else { - if (IsAlphaMaxForced && - component != ColorComponent.Alpha) - { - rgbColor = new Color(255, rgbColor.R, rgbColor.G, rgbColor.B); - } - byte componentValue = Convert.ToByte(MathUtilities.Clamp(sliderPercent * 255, 0, 255)); switch (component) { + case ColorComponent.Alpha: + rgbColor = new Color(componentValue, baseRgbColor.R, baseRgbColor.G, baseRgbColor.B); + break; case ColorComponent.Component1: - rgbColor = new Color(rgbColor.A, componentValue, rgbColor.G, rgbColor.B); + rgbColor = new Color(baseRgbColor.A, componentValue, baseRgbColor.G, baseRgbColor.B); break; case ColorComponent.Component2: - rgbColor = new Color(rgbColor.A, rgbColor.R, componentValue, rgbColor.B); + rgbColor = new Color(baseRgbColor.A, baseRgbColor.R, componentValue, baseRgbColor.B); break; case ColorComponent.Component3: - rgbColor = new Color(rgbColor.A, rgbColor.R, rgbColor.G, componentValue); + rgbColor = new Color(baseRgbColor.A, baseRgbColor.R, baseRgbColor.G, componentValue); break; } - selectedRgbColor = rgbColor; + return (rgbColor, rgbColor.ToHsv()); } - - //var converter = new ContrastBrushConverter(); - //this.Foreground = converter.Convert(selectedRgbColor, typeof(Brush), this.DefaultForeground, null) as Brush; - - return; } /// - /// Generates a new background image for the color slider and applies it. + /// Gets the actual background color displayed for the given HSV color. + /// This can differ due to the effects of certain properties intended to improve perception. /// - private async void UpdateBackground(HsvColor color) + /// The actual color to get the equivalent background color for. + /// The equivalent, perceived background color. + private HsvColor GetEquivalentBackgroundColor(HsvColor hsvColor) { - // Updates may be requested when sliders are not in the visual tree. - // For first-time load this is handled by the Loaded event. - // However, after that problems may arise, consider the following case: - // - // (1) Backgrounds are drawn normally the first time on Loaded. - // Actual height/width are available. - // (2) The palette tab is selected which has no sliders - // (3) The picker flyout is closed - // (4) Externally the color is changed - // The color change will trigger slider background updates but - // with the flyout closed, actual height/width are zero. - // No zero size bitmap can be generated. - // (5) The picker flyout is re-opened by the user and the default - // last-opened tab will be viewed: palette. - // No loaded events will be fired for sliders. The color change - // event was already handled in (4). The sliders will never - // be updated. - // - // In this case the sliders become out of sync with the Color because there is no way - // to tell when they actually come into view. To work around this, force a re-render of - // the background with the last size of the slider. This last size will be when it was - // last loaded or updated. - // - // In the future additional consideration may be required for SizeChanged of the control. - // This work-around will also cause issues if display scaling changes in the special - // case where cached sizes are required. - - var width = Convert.ToInt32(Bounds.Width); - var height = Convert.ToInt32(Bounds.Height); - - if (width == 0 || height == 0) + var component = ColorComponent; + var isAlphaMaxForced = IsAlphaMaxForced; + var isSaturationValueMaxForced = IsSaturationValueMaxForced; + + if (isAlphaMaxForced && + component != ColorComponent.Alpha) { - // Attempt to use the last size if it was available - if (cachedSize.IsDefault == false) - { - width = Convert.ToInt32(cachedSize.Width); - height = Convert.ToInt32(cachedSize.Height); - } + hsvColor = new HsvColor(1.0, hsvColor.H, hsvColor.S, hsvColor.V); } - else + + switch (component) { - cachedSize = new Size(width, height); + case ColorComponent.Component1: + return new HsvColor( + hsvColor.A, + hsvColor.H, + isSaturationValueMaxForced ? 1.0 : hsvColor.S, + isSaturationValueMaxForced ? 1.0 : hsvColor.V); + case ColorComponent.Component2: + return new HsvColor( + hsvColor.A, + hsvColor.H, + hsvColor.S, + isSaturationValueMaxForced ? 1.0 : hsvColor.V); + case ColorComponent.Component3: + return new HsvColor( + hsvColor.A, + hsvColor.H, + isSaturationValueMaxForced ? 1.0 : hsvColor.S, + hsvColor.V); + default: + return hsvColor; } + } + + /// + /// Gets the actual background color displayed for the given RGB color. + /// This can differ due to the effects of certain properties intended to improve perception. + /// + /// The actual color to get the equivalent background color for. + /// The equivalent, perceived background color. + private Color GetEquivalentBackgroundColor(Color rgbColor) + { + var component = ColorComponent; + var isAlphaMaxForced = IsAlphaMaxForced; - var bitmap = await ColorHelpers.CreateComponentBitmapAsync( - width, - height, - Orientation, - ColorModel, - ColorComponent, - color, - IsAlphaMaxForced, - IsSaturationValueMaxForced); - - if (bitmap != null) + if (isAlphaMaxForced && + component != ColorComponent.Alpha) { - Background = ColorHelpers.BitmapToBrushAsync(bitmap, width, height); + rgbColor = new Color(255, rgbColor.R, rgbColor.G, rgbColor.B); } - return; + return rgbColor; } /// protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) { - bool update = false; + if (disableUpdates) + { + base.OnPropertyChanged(change); + return; + } + // Always keep the two color properties in sync if (change.Property == ColorProperty) { - // Sync with HSV (which is primary) + disableUpdates = true; + HsvColor = Color.ToHsv(); - update = true; + + if (IsAutoUpdatingEnabled) + { + SetColorToSliderValues(); + UpdateBackground(); + } + + disableUpdates = false; } else if (change.Property == HsvColorProperty) { - update = true; + disableUpdates = true; + + Color = HsvColor.ToRgb(); + + if (IsAutoUpdatingEnabled) + { + SetColorToSliderValues(); + UpdateBackground(); + } + + disableUpdates = false; } else if (change.Property == BoundsProperty) { - update = true; + if (IsAutoUpdatingEnabled) + { + UpdateBackground(); + } } - - if (update && IsAutoUpdatingEnabled) + else if (change.Property == ValueProperty || + change.Property == MinimumProperty || + change.Property == MaximumProperty) { - UpdateColors(); + disableUpdates = true; + + (var color, var hsvColor) = GetColorFromSliderValues(); + + if (ColorModel == ColorModel.Hsva) + { + HsvColor = hsvColor; + Color = hsvColor.ToRgb(); + } + else + { + Color = color; + HsvColor = color.ToHsv(); + } + + disableUpdates = false; } base.OnPropertyChanged(change); diff --git a/src/Avalonia.Controls.ColorPicker/Helpers/ColorHelpers.cs b/src/Avalonia.Controls.ColorPicker/Helpers/ColorHelpers.cs index 37c6f552d6..6500d10fe9 100644 --- a/src/Avalonia.Controls.ColorPicker/Helpers/ColorHelpers.cs +++ b/src/Avalonia.Controls.ColorPicker/Helpers/ColorHelpers.cs @@ -719,7 +719,7 @@ namespace Avalonia.Controls.Primitives var brush = new ImageBrush(bitmap) { - Stretch = Stretch.Fill + Stretch = Stretch.None }; return brush;