diff --git a/src/Avalonia.Controls.ColorPicker/Avalonia.Controls.ColorPicker.csproj b/src/Avalonia.Controls.ColorPicker/Avalonia.Controls.ColorPicker.csproj index 473190fe59..b6aa1fc9a4 100644 --- a/src/Avalonia.Controls.ColorPicker/Avalonia.Controls.ColorPicker.csproj +++ b/src/Avalonia.Controls.ColorPicker/Avalonia.Controls.ColorPicker.csproj @@ -23,6 +23,7 @@ + diff --git a/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorPicker.xaml b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorPicker.xaml index b74e4e59aa..b2d404d6f3 100644 --- a/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorPicker.xaml +++ b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorPicker.xaml @@ -122,7 +122,7 @@ AutomationProperties.Name="Third Component" Grid.Column="0" IsAlphaVisible="False" - IsPerceptive="True" + IsPerceptive="False" Orientation="Vertical" ColorModel="Hsva" ColorComponent="{Binding ThirdComponent, ElementName=ColorSpectrum}" diff --git a/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorView.xaml b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorView.xaml index ad517e8e80..68de2f2644 100644 --- a/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorView.xaml +++ b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorView.xaml @@ -366,7 +366,7 @@ AutomationProperties.Name="Third Component" Grid.Column="0" IsAlphaVisible="False" - IsPerceptive="True" + IsPerceptive="False" Orientation="Vertical" ColorModel="Hsva" ColorComponent="{Binding ThirdComponent, ElementName=ColorSpectrum}" diff --git a/src/Avalonia.Controls.ColorPicker/Themes/Simple/ColorPicker.xaml b/src/Avalonia.Controls.ColorPicker/Themes/Simple/ColorPicker.xaml index c2535086fa..69370ebc7e 100644 --- a/src/Avalonia.Controls.ColorPicker/Themes/Simple/ColorPicker.xaml +++ b/src/Avalonia.Controls.ColorPicker/Themes/Simple/ColorPicker.xaml @@ -123,7 +123,7 @@ AutomationProperties.Name="Third Component" Grid.Column="0" IsAlphaVisible="False" - IsPerceptive="True" + IsPerceptive="False" Orientation="Vertical" ColorModel="Hsva" ColorComponent="{Binding ThirdComponent, ElementName=ColorSpectrum}" diff --git a/src/Avalonia.Controls.ColorPicker/Themes/Simple/ColorView.xaml b/src/Avalonia.Controls.ColorPicker/Themes/Simple/ColorView.xaml index 095c82a176..ef3e69b927 100644 --- a/src/Avalonia.Controls.ColorPicker/Themes/Simple/ColorView.xaml +++ b/src/Avalonia.Controls.ColorPicker/Themes/Simple/ColorView.xaml @@ -329,7 +329,7 @@ AutomationProperties.Name="Third Component" Grid.Column="0" IsAlphaVisible="False" - IsPerceptive="True" + IsPerceptive="False" Orientation="Vertical" ColorModel="Hsva" ColorComponent="{Binding ThirdComponent, ElementName=ColorSpectrum}" diff --git a/tests/Avalonia.Controls.UnitTests/Avalonia.Controls.UnitTests.csproj b/tests/Avalonia.Controls.UnitTests/Avalonia.Controls.UnitTests.csproj index a92ba0dc83..948c413c82 100644 --- a/tests/Avalonia.Controls.UnitTests/Avalonia.Controls.UnitTests.csproj +++ b/tests/Avalonia.Controls.UnitTests/Avalonia.Controls.UnitTests.csproj @@ -23,6 +23,7 @@ + diff --git a/tests/Avalonia.Controls.UnitTests/ColorPickerTests/ColorPickerHelpersTests.cs b/tests/Avalonia.Controls.UnitTests/ColorPickerTests/ColorPickerHelpersTests.cs new file mode 100644 index 0000000000..605c48caf6 --- /dev/null +++ b/tests/Avalonia.Controls.UnitTests/ColorPickerTests/ColorPickerHelpersTests.cs @@ -0,0 +1,110 @@ +using System; +using System.Threading.Tasks; +using Avalonia.Collections.Pooled; +using Avalonia.Controls.Primitives; +using Avalonia.Layout; +using Avalonia.Media; +using Avalonia.UnitTests; +using Xunit; + +namespace Avalonia.Controls.UnitTests.ColorPickerTests; + +/// +/// Tests that produces +/// accurate pixel output when isPerceptive=false — the mode the ColorSpectrum's +/// third-component slider should use. +/// +/// Regression tests for https://github.com/AvaloniaUI/Avalonia/issues/20925 +/// +public class ColorPickerHelpersTests : ScopedTestBase +{ + /// + /// When sweeping Hue at a fixed desaturated selection (S=0.2, V=0.5), + /// every pixel's brightest channel should be bounded by V — not boosted to 255. + /// + [Fact] + public async Task Hue_Slider_Bitmap_Reflects_Actual_Saturation_And_Value() + { + var selection = new HsvColor(alpha: 1.0, hue: 180, saturation: 0.2, value: 0.5); + + using var pixels = await GenerateSliderBitmap(ColorComponent.Component1, selection, height: 360); + + // Sample mid-strip. In HSV, max RGB channel = V for any hue. + var (r, g, b) = ReadPixel(pixels, row: 180); + var maxChannel = Math.Max(r, Math.Max(g, b)); + double expectedMax = selection.V * 255; + + Assert.InRange(maxChannel, expectedMax - 10, expectedMax + 10); + } + + /// + /// When sweeping Saturation, the brightest channel at full saturation is bounded + /// by the current Value — not forced to 255 by perceptive V=1.0 override. + /// + [Theory] + [InlineData(0.1)] + [InlineData(0.5)] + [InlineData(0.9)] + public async Task Saturation_Slider_Bitmap_Respects_Current_Value(double currentValue) + { + var selection = new HsvColor(alpha: 1.0, hue: 200, saturation: 0.8, value: currentValue); + + using var pixels = await GenerateSliderBitmap(ColorComponent.Component2, selection); + + // Row 0 = highest saturation (bitmap sweeps high→low top-to-bottom). + var (r, g, b) = ReadPixel(pixels, row: 0); + var maxChannel = Math.Max(r, Math.Max(g, b)); + double expectedMax = currentValue * 255; + + Assert.InRange(maxChannel, expectedMax - 20, expectedMax + 20); + } + + /// + /// When sweeping Value at low Saturation (S=0.1), the top pixel should be + /// near-grey (low chroma) — not vivid from a perceptive S=1.0 override. + /// + [Fact] + public async Task Value_Slider_Bitmap_Respects_Current_Saturation() + { + var selection = new HsvColor(alpha: 1.0, hue: 30, saturation: 0.1, value: 0.8); + + using var pixels = await GenerateSliderBitmap(ColorComponent.Component3, selection); + + // Row 0 = highest Value. In HSV, chroma = V * S * 255 ≤ S * 255. + var (r, g, b) = ReadPixel(pixels, row: 0); + int chroma = Math.Max(r, Math.Max(g, b)) - Math.Min(r, Math.Min(g, b)); + + // At S=0.1 the max possible chroma is 25.5 — near-grey for any V. + // With perceptive S=1.0 override, chroma would be ~252. + Assert.InRange(chroma, 0, selection.S * 255 + 10); + } + + private static async Task> GenerateSliderBitmap( + ColorComponent component, + HsvColor baseColor, + int height = 100) + { + const int width = 1; + var buffer = new PooledList(width * height * 4, ClearMode.Never, sizeToCapacity: true); + + await ColorPickerHelpers.CreateComponentBitmapAsync( + buffer, width, height, + Orientation.Vertical, + ColorModel.Hsva, + component, + baseColor, + isAlphaVisible: false, + isPerceptive: false); + + return buffer; + } + + /// + /// Reads a pixel from a 1-pixel-wide vertical BGRA bitmap at the given row. + /// + private static (byte R, byte G, byte B) ReadPixel(PooledList bgraData, int row) + { + int offset = row * 4; + return (R: bgraData[offset + 2], G: bgraData[offset + 1], B: bgraData[offset]); + } +}