diff --git a/Avalonia.sln b/Avalonia.sln
index 1e2a3c6027..ea30514c3e 100644
--- a/Avalonia.sln
+++ b/Avalonia.sln
@@ -169,6 +169,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PlatformSanityChecks", "sam
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.ReactiveUI.UnitTests", "tests\Avalonia.ReactiveUI.UnitTests\Avalonia.ReactiveUI.UnitTests.csproj", "{AF915D5C-AB00-4EA0-B5E6-001F4AE84E68}"
EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Controls.ColorPicker", "src\Avalonia.Controls.ColorPicker\Avalonia.Controls.ColorPicker.csproj", "{1ECC012A-8837-4AE2-9BDA-3E2857898727}"
+EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Controls.DataGrid", "src\Avalonia.Controls.DataGrid\Avalonia.Controls.DataGrid.csproj", "{3278F3A9-9509-4A3F-A15B-BDC8B5BFF632}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Dialogs", "src\Avalonia.Dialogs\Avalonia.Dialogs.csproj", "{4D55985A-1EE2-4F25-AD39-6EA8BC04F8FB}"
@@ -1963,6 +1965,30 @@ Global
{2B390431-288C-435C-BB6B-A374033BD8D1}.Release|iPhone.Build.0 = Release|Any CPU
{2B390431-288C-435C-BB6B-A374033BD8D1}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU
{2B390431-288C-435C-BB6B-A374033BD8D1}.Release|iPhoneSimulator.Build.0 = Release|Any CPU
+ {1ECC012A-8837-4AE2-9BDA-3E2857898727}.Ad-Hoc|Any CPU.ActiveCfg = Debug|Any CPU
+ {1ECC012A-8837-4AE2-9BDA-3E2857898727}.Ad-Hoc|Any CPU.Build.0 = Debug|Any CPU
+ {1ECC012A-8837-4AE2-9BDA-3E2857898727}.Ad-Hoc|iPhone.ActiveCfg = Debug|Any CPU
+ {1ECC012A-8837-4AE2-9BDA-3E2857898727}.Ad-Hoc|iPhone.Build.0 = Debug|Any CPU
+ {1ECC012A-8837-4AE2-9BDA-3E2857898727}.Ad-Hoc|iPhoneSimulator.ActiveCfg = Debug|Any CPU
+ {1ECC012A-8837-4AE2-9BDA-3E2857898727}.Ad-Hoc|iPhoneSimulator.Build.0 = Debug|Any CPU
+ {1ECC012A-8837-4AE2-9BDA-3E2857898727}.AppStore|Any CPU.ActiveCfg = Debug|Any CPU
+ {1ECC012A-8837-4AE2-9BDA-3E2857898727}.AppStore|Any CPU.Build.0 = Debug|Any CPU
+ {1ECC012A-8837-4AE2-9BDA-3E2857898727}.AppStore|iPhone.ActiveCfg = Debug|Any CPU
+ {1ECC012A-8837-4AE2-9BDA-3E2857898727}.AppStore|iPhone.Build.0 = Debug|Any CPU
+ {1ECC012A-8837-4AE2-9BDA-3E2857898727}.AppStore|iPhoneSimulator.ActiveCfg = Debug|Any CPU
+ {1ECC012A-8837-4AE2-9BDA-3E2857898727}.AppStore|iPhoneSimulator.Build.0 = Debug|Any CPU
+ {1ECC012A-8837-4AE2-9BDA-3E2857898727}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {1ECC012A-8837-4AE2-9BDA-3E2857898727}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {1ECC012A-8837-4AE2-9BDA-3E2857898727}.Debug|iPhone.ActiveCfg = Debug|Any CPU
+ {1ECC012A-8837-4AE2-9BDA-3E2857898727}.Debug|iPhone.Build.0 = Debug|Any CPU
+ {1ECC012A-8837-4AE2-9BDA-3E2857898727}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU
+ {1ECC012A-8837-4AE2-9BDA-3E2857898727}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU
+ {1ECC012A-8837-4AE2-9BDA-3E2857898727}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {1ECC012A-8837-4AE2-9BDA-3E2857898727}.Release|Any CPU.Build.0 = Release|Any CPU
+ {1ECC012A-8837-4AE2-9BDA-3E2857898727}.Release|iPhone.ActiveCfg = Release|Any CPU
+ {1ECC012A-8837-4AE2-9BDA-3E2857898727}.Release|iPhone.Build.0 = Release|Any CPU
+ {1ECC012A-8837-4AE2-9BDA-3E2857898727}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU
+ {1ECC012A-8837-4AE2-9BDA-3E2857898727}.Release|iPhoneSimulator.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
diff --git a/samples/ControlCatalog/App.xaml.cs b/samples/ControlCatalog/App.xaml.cs
index 866fb8632a..6539cdaee6 100644
--- a/samples/ControlCatalog/App.xaml.cs
+++ b/samples/ControlCatalog/App.xaml.cs
@@ -18,6 +18,16 @@ namespace ControlCatalog
DataContext = new ApplicationViewModel();
}
+ public static readonly StyleInclude ColorPickerFluent = new StyleInclude(new Uri("avares://ControlCatalog/Styles"))
+ {
+ Source = new Uri("avares://Avalonia.Controls.ColorPicker/Themes/Fluent.xaml")
+ };
+
+ public static readonly StyleInclude ColorPickerDefault = new StyleInclude(new Uri("avares://ControlCatalog/Styles"))
+ {
+ Source = new Uri("avares://Avalonia.Controls.ColorPicker/Themes/Default.xaml")
+ };
+
public static readonly StyleInclude DataGridFluent = new StyleInclude(new Uri("avares://ControlCatalog/Styles"))
{
Source = new Uri("avares://Avalonia.Controls.DataGrid/Themes/Fluent.xaml")
@@ -69,7 +79,8 @@ namespace ControlCatalog
public override void Initialize()
{
Styles.Insert(0, Fluent);
- Styles.Insert(1, DataGridFluent);
+ Styles.Insert(1, ColorPickerFluent);
+ Styles.Insert(2, DataGridFluent);
AvaloniaXamlLoader.Load(this);
}
diff --git a/samples/ControlCatalog/ControlCatalog.csproj b/samples/ControlCatalog/ControlCatalog.csproj
index c0e24357ca..7cbd8a3f9c 100644
--- a/samples/ControlCatalog/ControlCatalog.csproj
+++ b/samples/ControlCatalog/ControlCatalog.csproj
@@ -23,6 +23,7 @@
+
diff --git a/samples/ControlCatalog/MainView.xaml b/samples/ControlCatalog/MainView.xaml
index 85f278b5fa..59d724db69 100644
--- a/samples/ControlCatalog/MainView.xaml
+++ b/samples/ControlCatalog/MainView.xaml
@@ -43,6 +43,9 @@
+
+
+
diff --git a/samples/ControlCatalog/MainView.xaml.cs b/samples/ControlCatalog/MainView.xaml.cs
index e8ea39abbb..0326946c08 100644
--- a/samples/ControlCatalog/MainView.xaml.cs
+++ b/samples/ControlCatalog/MainView.xaml.cs
@@ -49,7 +49,8 @@ namespace ControlCatalog
App.Fluent.Mode = FluentThemeMode.Light;
}
Application.Current.Styles[0] = App.Fluent;
- Application.Current.Styles[1] = App.DataGridFluent;
+ Application.Current.Styles[1] = App.ColorPickerFluent;
+ Application.Current.Styles[2] = App.DataGridFluent;
}
else if (theme == CatalogTheme.FluentDark)
{
@@ -59,19 +60,22 @@ namespace ControlCatalog
App.Fluent.Mode = FluentThemeMode.Dark;
}
Application.Current.Styles[0] = App.Fluent;
- Application.Current.Styles[1] = App.DataGridFluent;
+ Application.Current.Styles[1] = App.ColorPickerFluent;
+ Application.Current.Styles[2] = App.DataGridFluent;
}
else if (theme == CatalogTheme.DefaultLight)
{
App.Default.Mode = Avalonia.Themes.Default.SimpleThemeMode.Light;
Application.Current.Styles[0] = App.DefaultLight;
- Application.Current.Styles[1] = App.DataGridDefault;
+ Application.Current.Styles[1] = App.ColorPickerDefault;
+ Application.Current.Styles[2] = App.DataGridDefault;
}
else if (theme == CatalogTheme.DefaultDark)
{
App.Default.Mode = Avalonia.Themes.Default.SimpleThemeMode.Dark;
Application.Current.Styles[0] = App.DefaultDark;
- Application.Current.Styles[1] = App.DataGridDefault;
+ Application.Current.Styles[1] = App.ColorPickerDefault;
+ Application.Current.Styles[2] = App.DataGridDefault;
}
}
};
diff --git a/samples/ControlCatalog/Pages/ColorPickerPage.xaml b/samples/ControlCatalog/Pages/ColorPickerPage.xaml
new file mode 100644
index 0000000000..ec34193f8c
--- /dev/null
+++ b/samples/ControlCatalog/Pages/ColorPickerPage.xaml
@@ -0,0 +1,29 @@
+
+
+
+
+
+
+
+
diff --git a/samples/ControlCatalog/Pages/ColorPickerPage.xaml.cs b/samples/ControlCatalog/Pages/ColorPickerPage.xaml.cs
new file mode 100644
index 0000000000..6e017e381f
--- /dev/null
+++ b/samples/ControlCatalog/Pages/ColorPickerPage.xaml.cs
@@ -0,0 +1,19 @@
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Markup.Xaml;
+
+namespace ControlCatalog.Pages
+{
+ public partial class ColorPickerPage : UserControl
+ {
+ public ColorPickerPage()
+ {
+ InitializeComponent();
+ }
+
+ private void InitializeComponent()
+ {
+ AvaloniaXamlLoader.Load(this);
+ }
+ }
+}
diff --git a/samples/Sandbox/Sandbox.csproj b/samples/Sandbox/Sandbox.csproj
index 8f2812e048..f3c38cd96e 100644
--- a/samples/Sandbox/Sandbox.csproj
+++ b/samples/Sandbox/Sandbox.csproj
@@ -8,6 +8,7 @@
+
diff --git a/src/Avalonia.Base/Layout/LayoutHelper.cs b/src/Avalonia.Base/Layout/LayoutHelper.cs
index d4154a6d0c..d24be57d2b 100644
--- a/src/Avalonia.Base/Layout/LayoutHelper.cs
+++ b/src/Avalonia.Base/Layout/LayoutHelper.cs
@@ -52,16 +52,44 @@ namespace Avalonia.Layout
public static Size ArrangeChild(ILayoutable? child, Size availableSize, Thickness padding, Thickness borderThickness)
{
- return ArrangeChild(child, availableSize, padding + borderThickness);
+ if (IsParentLayoutRounded(child, out double scale))
+ {
+ padding = RoundLayoutThickness(padding, scale, scale);
+ borderThickness = RoundLayoutThickness(borderThickness, scale, scale);
+ }
+
+ return ArrangeChildInternal(child, availableSize, padding + borderThickness);
}
public static Size ArrangeChild(ILayoutable? child, Size availableSize, Thickness padding)
+ {
+ if(IsParentLayoutRounded(child, out double scale))
+ padding = RoundLayoutThickness(padding, scale, scale);
+
+ return ArrangeChildInternal(child, availableSize, padding);
+ }
+
+ private static Size ArrangeChildInternal(ILayoutable? child, Size availableSize, Thickness padding)
{
child?.Arrange(new Rect(availableSize).Deflate(padding));
return availableSize;
}
+ private static bool IsParentLayoutRounded(ILayoutable? child, out double scale)
+ {
+ var layoutableParent = (ILayoutable?)child?.GetVisualParent();
+
+ if (layoutableParent == null || !((Layoutable)layoutableParent).UseLayoutRounding)
+ {
+ scale = 1.0;
+ return false;
+ }
+
+ scale = GetLayoutScale(layoutableParent);
+ return true;
+ }
+
///
/// Invalidates measure for given control and all visual children recursively.
///
@@ -126,6 +154,32 @@ namespace Avalonia.Layout
return new Size(RoundLayoutValue(size.Width, dpiScaleX), RoundLayoutValue(size.Height, dpiScaleY));
}
+ ///
+ /// Rounds a thickness to integer values for layout purposes, compensating for high DPI screen
+ /// coordinates.
+ ///
+ /// Input thickness.
+ /// DPI along x-dimension.
+ /// DPI along y-dimension.
+ /// Value of thickness that will be rounded under screen DPI.
+ ///
+ /// This is a layout helper method. It takes DPI into account and also does not return
+ /// the rounded value if it is unacceptable for layout, e.g. Infinity or NaN. It's a helper
+ /// associated with the UseLayoutRounding property and should not be used as a general rounding
+ /// utility.
+ ///
+ public static Thickness RoundLayoutThickness(Thickness thickness, double dpiScaleX, double dpiScaleY)
+ {
+ return new Thickness(
+ RoundLayoutValue(thickness.Left, dpiScaleX),
+ RoundLayoutValue(thickness.Top, dpiScaleY),
+ RoundLayoutValue(thickness.Right, dpiScaleX),
+ RoundLayoutValue(thickness.Bottom, dpiScaleY)
+ );
+ }
+
+
+
///
/// Calculates the value to be used for layout rounding at high DPI.
///
@@ -163,8 +217,7 @@ namespace Avalonia.Layout
return newValue;
}
-
-
+
///
/// Calculates the min and max height for a control. Ported from WPF.
///
diff --git a/src/Avalonia.Base/Layout/Layoutable.cs b/src/Avalonia.Base/Layout/Layoutable.cs
index 09e0c4263a..df7aa937a0 100644
--- a/src/Avalonia.Base/Layout/Layoutable.cs
+++ b/src/Avalonia.Base/Layout/Layoutable.cs
@@ -643,17 +643,27 @@ namespace Avalonia.Layout
{
if (IsVisible)
{
+ var useLayoutRounding = UseLayoutRounding;
+ var scale = LayoutHelper.GetLayoutScale(this);
+
var margin = Margin;
var originX = finalRect.X + margin.Left;
var originY = finalRect.Y + margin.Top;
+
+ // Margin has to be treated separately because the layout rounding function is not linear
+ // f(a + b) != f(a) + f(b)
+ // If the margin isn't pre-rounded some sizes will be offset by 1 pixel in certain scales.
+ if (useLayoutRounding)
+ {
+ margin = LayoutHelper.RoundLayoutThickness(margin, scale, scale);
+ }
+
var availableSizeMinusMargins = new Size(
Math.Max(0, finalRect.Width - margin.Left - margin.Right),
Math.Max(0, finalRect.Height - margin.Top - margin.Bottom));
var horizontalAlignment = HorizontalAlignment;
var verticalAlignment = VerticalAlignment;
var size = availableSizeMinusMargins;
- var scale = LayoutHelper.GetLayoutScale(this);
- var useLayoutRounding = UseLayoutRounding;
if (horizontalAlignment != HorizontalAlignment.Stretch)
{
diff --git a/src/Avalonia.Base/Properties/AssemblyInfo.cs b/src/Avalonia.Base/Properties/AssemblyInfo.cs
index a0560924e7..2c40c768f5 100644
--- a/src/Avalonia.Base/Properties/AssemblyInfo.cs
+++ b/src/Avalonia.Base/Properties/AssemblyInfo.cs
@@ -19,6 +19,7 @@ using Avalonia.Metadata;
[assembly: InternalsVisibleTo("Avalonia.Base.UnitTests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c1bba1142285fe0419326fb25866ba62c47e6c2b5c1ab0c95b46413fad375471232cb81706932e1cef38781b9ebd39d5100401bacb651c6c5bbf59e571e81b3bc08d2a622004e08b1a6ece82a7e0b9857525c86d2b95fab4bc3dce148558d7f3ae61aa3a234086902aeface87d9dfdd32b9d2fe3c6dd4055b5ab4b104998bd87")]
[assembly: InternalsVisibleTo("Avalonia.Controls, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c1bba1142285fe0419326fb25866ba62c47e6c2b5c1ab0c95b46413fad375471232cb81706932e1cef38781b9ebd39d5100401bacb651c6c5bbf59e571e81b3bc08d2a622004e08b1a6ece82a7e0b9857525c86d2b95fab4bc3dce148558d7f3ae61aa3a234086902aeface87d9dfdd32b9d2fe3c6dd4055b5ab4b104998bd87")]
+[assembly: InternalsVisibleTo("Avalonia.Controls.ColorPicker, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c1bba1142285fe0419326fb25866ba62c47e6c2b5c1ab0c95b46413fad375471232cb81706932e1cef38781b9ebd39d5100401bacb651c6c5bbf59e571e81b3bc08d2a622004e08b1a6ece82a7e0b9857525c86d2b95fab4bc3dce148558d7f3ae61aa3a234086902aeface87d9dfdd32b9d2fe3c6dd4055b5ab4b104998bd87")]
[assembly: InternalsVisibleTo("Avalonia.Controls.DataGrid, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c1bba1142285fe0419326fb25866ba62c47e6c2b5c1ab0c95b46413fad375471232cb81706932e1cef38781b9ebd39d5100401bacb651c6c5bbf59e571e81b3bc08d2a622004e08b1a6ece82a7e0b9857525c86d2b95fab4bc3dce148558d7f3ae61aa3a234086902aeface87d9dfdd32b9d2fe3c6dd4055b5ab4b104998bd87")]
[assembly: InternalsVisibleTo("Avalonia.Controls.UnitTests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c1bba1142285fe0419326fb25866ba62c47e6c2b5c1ab0c95b46413fad375471232cb81706932e1cef38781b9ebd39d5100401bacb651c6c5bbf59e571e81b3bc08d2a622004e08b1a6ece82a7e0b9857525c86d2b95fab4bc3dce148558d7f3ae61aa3a234086902aeface87d9dfdd32b9d2fe3c6dd4055b5ab4b104998bd87")]
[assembly: InternalsVisibleTo("Avalonia.DesignerSupport, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c1bba1142285fe0419326fb25866ba62c47e6c2b5c1ab0c95b46413fad375471232cb81706932e1cef38781b9ebd39d5100401bacb651c6c5bbf59e571e81b3bc08d2a622004e08b1a6ece82a7e0b9857525c86d2b95fab4bc3dce148558d7f3ae61aa3a234086902aeface87d9dfdd32b9d2fe3c6dd4055b5ab4b104998bd87")]
diff --git a/src/Avalonia.Controls.ColorPicker/Avalonia.Controls.ColorPicker.csproj b/src/Avalonia.Controls.ColorPicker/Avalonia.Controls.ColorPicker.csproj
new file mode 100644
index 0000000000..0952c899d4
--- /dev/null
+++ b/src/Avalonia.Controls.ColorPicker/Avalonia.Controls.ColorPicker.csproj
@@ -0,0 +1,25 @@
+
+
+ net6.0;netstandard2.0
+ Avalonia.Controls.ColorPicker
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Avalonia.Controls.ColorPicker/ColorChangedEventArgs.cs b/src/Avalonia.Controls.ColorPicker/ColorChangedEventArgs.cs
new file mode 100644
index 0000000000..b1d15d6b17
--- /dev/null
+++ b/src/Avalonia.Controls.ColorPicker/ColorChangedEventArgs.cs
@@ -0,0 +1,41 @@
+// Portions of this source file are adapted from the WinUI project.
+// (https://github.com/microsoft/microsoft-ui-xaml)
+//
+// Licensed to The Avalonia Project under the MIT License.
+
+using System;
+using Avalonia.Media;
+
+namespace Avalonia.Controls
+{
+ ///
+ /// Holds the details of a ColorChanged event.
+ ///
+ ///
+ /// HSV color information is intentionally not provided.
+ /// Use to obtain it.
+ ///
+ public class ColorChangedEventArgs : EventArgs
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The old/original color from before the change event.
+ /// The new/updated color that triggered the change event.
+ public ColorChangedEventArgs(Color oldColor, Color newColor)
+ {
+ OldColor = oldColor;
+ NewColor = newColor;
+ }
+
+ ///
+ /// Gets the old/original color from before the change event.
+ ///
+ public Color OldColor { get; private set; }
+
+ ///
+ /// Gets the new/updated color that triggered the change event.
+ ///
+ public Color NewColor { get; private set; }
+ }
+}
diff --git a/src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorHelpers.cs b/src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorHelpers.cs
new file mode 100644
index 0000000000..b912d39aba
--- /dev/null
+++ b/src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorHelpers.cs
@@ -0,0 +1,414 @@
+// This source file is adapted from the WinUI project.
+// (https://github.com/microsoft/microsoft-ui-xaml)
+//
+// Licensed to The Avalonia Project under the MIT License.
+
+using System;
+using System.Collections.Generic;
+using System.Runtime.InteropServices;
+using Avalonia.Media;
+using Avalonia.Media.Imaging;
+using Avalonia.Platform;
+
+namespace Avalonia.Controls.Primitives
+{
+ internal static class ColorHelpers
+ {
+ public const int CheckerSize = 4;
+
+ public static bool ToDisplayNameExists
+ {
+ get => false;
+ }
+
+ public static string ToDisplayName(Color color)
+ {
+ return string.Empty;
+ }
+
+ public static Hsv IncrementColorComponent(
+ Hsv originalHsv,
+ HsvComponent component,
+ IncrementDirection direction,
+ IncrementAmount amount,
+ bool shouldWrap,
+ double minBound,
+ double maxBound)
+ {
+ Hsv newHsv = originalHsv;
+
+ if (amount == IncrementAmount.Small || !ToDisplayNameExists)
+ {
+ // In order to avoid working with small values that can incur rounding issues,
+ // we'll multiple saturation and value by 100 to put them in the range of 0-100 instead of 0-1.
+ newHsv.S *= 100;
+ newHsv.V *= 100;
+
+ // Note: *valueToIncrement replaced with ref local variable for C#, must be initialized
+ ref double valueToIncrement = ref newHsv.H;
+ double incrementAmount = 0.0;
+
+ // If we're adding a small increment, then we'll just add or subtract 1.
+ // If we're adding a large increment, then we want to snap to the next
+ // or previous major value - for hue, this is every increment of 30;
+ // for saturation and value, this is every increment of 10.
+ switch (component)
+ {
+ case HsvComponent.Hue:
+ valueToIncrement = ref newHsv.H;
+ incrementAmount = amount == IncrementAmount.Small ? 1 : 30;
+ break;
+
+ case HsvComponent.Saturation:
+ valueToIncrement = ref newHsv.S;
+ incrementAmount = amount == IncrementAmount.Small ? 1 : 10;
+ break;
+
+ case HsvComponent.Value:
+ valueToIncrement = ref newHsv.V;
+ incrementAmount = amount == IncrementAmount.Small ? 1 : 10;
+ break;
+
+ default:
+ throw new InvalidOperationException("Invalid HsvComponent.");
+ }
+
+ double previousValue = valueToIncrement;
+
+ valueToIncrement += (direction == IncrementDirection.Lower ? -incrementAmount : incrementAmount);
+
+ // If the value has reached outside the bounds, we were previous at the boundary, and we should wrap,
+ // then we'll place the selection on the other side of the spectrum.
+ // Otherwise, we'll place it on the boundary that was exceeded.
+ if (valueToIncrement < minBound)
+ {
+ valueToIncrement = (shouldWrap && previousValue == minBound) ? maxBound : minBound;
+ }
+
+ if (valueToIncrement > maxBound)
+ {
+ valueToIncrement = (shouldWrap && previousValue == maxBound) ? minBound : maxBound;
+ }
+
+ // We multiplied saturation and value by 100 previously, so now we want to put them back in the 0-1 range.
+ newHsv.S /= 100;
+ newHsv.V /= 100;
+ }
+ else
+ {
+ // While working with named colors, we're going to need to be working in actual HSV units,
+ // so we'll divide the min bound and max bound by 100 in the case of saturation or value,
+ // since we'll have received units between 0-100 and we need them within 0-1.
+ if (component == HsvComponent.Saturation ||
+ component == HsvComponent.Value)
+ {
+ minBound /= 100;
+ maxBound /= 100;
+ }
+
+ newHsv = FindNextNamedColor(originalHsv, component, direction, shouldWrap, minBound, maxBound);
+ }
+
+ return newHsv;
+ }
+
+ public static Hsv FindNextNamedColor(
+ Hsv originalHsv,
+ HsvComponent component,
+ IncrementDirection direction,
+ bool shouldWrap,
+ double minBound,
+ double maxBound)
+ {
+ // There's no easy way to directly get the next named color, so what we'll do
+ // is just iterate in the direction that we want to find it until we find a color
+ // in that direction that has a color name different than our current color name.
+ // Once we find a new color name, then we'll iterate across that color name until
+ // we find its bounds on the other side, and then select the color that is exactly
+ // in the middle of that color's bounds.
+ Hsv newHsv = originalHsv;
+
+ string originalColorName = ColorHelpers.ToDisplayName(originalHsv.ToRgb().ToColor());
+ string newColorName = originalColorName;
+
+ // Note: *newValue replaced with ref local variable for C#, must be initialized
+ double originalValue = 0.0;
+ ref double newValue = ref newHsv.H;
+ double incrementAmount = 0.0;
+
+ switch (component)
+ {
+ case HsvComponent.Hue:
+ originalValue = originalHsv.H;
+ newValue = ref newHsv.H;
+ incrementAmount = 1;
+ break;
+
+ case HsvComponent.Saturation:
+ originalValue = originalHsv.S;
+ newValue = ref newHsv.S;
+ incrementAmount = 0.01;
+ break;
+
+ case HsvComponent.Value:
+ originalValue = originalHsv.V;
+ newValue = ref newHsv.V;
+ incrementAmount = 0.01;
+ break;
+
+ default:
+ throw new InvalidOperationException("Invalid HsvComponent.");
+ }
+
+ bool shouldFindMidPoint = true;
+
+ while (newColorName == originalColorName)
+ {
+ double previousValue = newValue;
+ newValue += (direction == IncrementDirection.Lower ? -1 : 1) * incrementAmount;
+
+ bool justWrapped = false;
+
+ // If we've hit a boundary, then either we should wrap or we shouldn't.
+ // If we should, then we'll perform that wrapping if we were previously up against
+ // the boundary that we've now hit. Otherwise, we'll stop at that boundary.
+ if (newValue > maxBound)
+ {
+ if (shouldWrap)
+ {
+ newValue = minBound;
+ justWrapped = true;
+ }
+ else
+ {
+ newValue = maxBound;
+ shouldFindMidPoint = false;
+ newColorName = ColorHelpers.ToDisplayName(newHsv.ToRgb().ToColor());
+ break;
+ }
+ }
+ else if (newValue < minBound)
+ {
+ if (shouldWrap)
+ {
+ newValue = maxBound;
+ justWrapped = true;
+ }
+ else
+ {
+ newValue = minBound;
+ shouldFindMidPoint = false;
+ newColorName = ColorHelpers.ToDisplayName(newHsv.ToRgb().ToColor());
+ break;
+ }
+ }
+
+ if (!justWrapped &&
+ previousValue != originalValue &&
+ Math.Sign(newValue - originalValue) != Math.Sign(previousValue - originalValue))
+ {
+ // If we've wrapped all the way back to the start and have failed to find a new color name,
+ // then we'll just quit - there isn't a new color name that we're going to find.
+ shouldFindMidPoint = false;
+ break;
+ }
+
+ newColorName = ColorHelpers.ToDisplayName(newHsv.ToRgb().ToColor());
+ }
+
+ if (shouldFindMidPoint)
+ {
+ Hsv startHsv = newHsv;
+ Hsv currentHsv = startHsv;
+ double startEndOffset = 0;
+ string currentColorName = newColorName;
+
+ // Note: *startValue/*currentValue replaced with ref local variables for C#, must be initialized
+ ref double startValue = ref startHsv.H;
+ ref double currentValue = ref currentHsv.H;
+ double wrapIncrement = 0;
+
+ switch (component)
+ {
+ case HsvComponent.Hue:
+ startValue = ref startHsv.H;
+ currentValue = ref currentHsv.H;
+ wrapIncrement = 360.0;
+ break;
+
+ case HsvComponent.Saturation:
+ startValue = ref startHsv.S;
+ currentValue = ref currentHsv.S;
+ wrapIncrement = 1.0;
+ break;
+
+ case HsvComponent.Value:
+ startValue = ref startHsv.V;
+ currentValue = ref currentHsv.V;
+ wrapIncrement = 1.0;
+ break;
+
+ default:
+ throw new InvalidOperationException("Invalid HsvComponent.");
+ }
+
+ while (newColorName == currentColorName)
+ {
+ currentValue += (direction == IncrementDirection.Lower ? -1 : 1) * incrementAmount;
+
+ // If we've hit a boundary, then either we should wrap or we shouldn't.
+ // If we should, then we'll perform that wrapping if we were previously up against
+ // the boundary that we've now hit. Otherwise, we'll stop at that boundary.
+ if (currentValue > maxBound)
+ {
+ if (shouldWrap)
+ {
+ currentValue = minBound;
+ startEndOffset = maxBound - minBound;
+ }
+ else
+ {
+ currentValue = maxBound;
+ break;
+ }
+ }
+ else if (currentValue < minBound)
+ {
+ if (shouldWrap)
+ {
+ currentValue = maxBound;
+ startEndOffset = minBound - maxBound;
+ }
+ else
+ {
+ currentValue = minBound;
+ break;
+ }
+ }
+
+ currentColorName = ColorHelpers.ToDisplayName(currentHsv.ToRgb().ToColor());
+ }
+
+ newValue = (startValue + currentValue + startEndOffset) / 2;
+
+ // Dividing by 2 may have gotten us halfway through a single step, so we'll
+ // remove that half-step if it exists.
+ double leftoverValue = Math.Abs(newValue);
+
+ while (leftoverValue > incrementAmount)
+ {
+ leftoverValue -= incrementAmount;
+ }
+
+ newValue -= leftoverValue;
+
+ while (newValue < minBound)
+ {
+ newValue += wrapIncrement;
+ }
+
+ while (newValue > maxBound)
+ {
+ newValue -= wrapIncrement;
+ }
+ }
+
+ return newHsv;
+ }
+
+ public static double IncrementAlphaComponent(
+ double originalAlpha,
+ IncrementDirection direction,
+ IncrementAmount amount,
+ bool shouldWrap,
+ double minBound,
+ double maxBound)
+ {
+ // In order to avoid working with small values that can incur rounding issues,
+ // we'll multiple alpha by 100 to put it in the range of 0-100 instead of 0-1.
+ originalAlpha *= 100;
+
+ const double smallIncrementAmount = 1;
+ const double largeIncrementAmount = 10;
+
+ if (amount == IncrementAmount.Small)
+ {
+ originalAlpha += (direction == IncrementDirection.Lower ? -1 : 1) * smallIncrementAmount;
+ }
+ else
+ {
+ if (direction == IncrementDirection.Lower)
+ {
+ originalAlpha = Math.Ceiling((originalAlpha - largeIncrementAmount) / largeIncrementAmount) * largeIncrementAmount;
+ }
+ else
+ {
+ originalAlpha = Math.Floor((originalAlpha + largeIncrementAmount) / largeIncrementAmount) * largeIncrementAmount;
+ }
+ }
+
+ // If the value has reached outside the bounds and we should wrap, then we'll place the selection
+ // on the other side of the spectrum. Otherwise, we'll place it on the boundary that was exceeded.
+ if (originalAlpha < minBound)
+ {
+ originalAlpha = shouldWrap ? maxBound : minBound;
+ }
+
+ if (originalAlpha > maxBound)
+ {
+ originalAlpha = shouldWrap ? minBound : maxBound;
+ }
+
+ // We multiplied alpha by 100 previously, so now we want to put it back in the 0-1 range.
+ return originalAlpha / 100;
+ }
+
+ public static WriteableBitmap CreateBitmapFromPixelData(
+ int pixelWidth,
+ int pixelHeight,
+ List bgraPixelData)
+ {
+ Vector dpi = new Vector(96, 96); // Standard may need to change on some devices
+
+ WriteableBitmap bitmap = new WriteableBitmap(
+ new PixelSize(pixelWidth, pixelHeight),
+ dpi,
+ PixelFormat.Bgra8888,
+ AlphaFormat.Premul);
+
+ // Warning: This is highly questionable
+ using (var frameBuffer = bitmap.Lock())
+ {
+ Marshal.Copy(bgraPixelData.ToArray(), 0, frameBuffer.Address, bgraPixelData.Count);
+ }
+
+ return bitmap;
+ }
+
+ ///
+ /// Gets the relative (perceptual) luminance/brightness of the given color.
+ /// 1 is closer to white while 0 is closer to black.
+ ///
+ /// The color to calculate relative luminance for.
+ /// The relative (perceptual) luminance/brightness of the given color.
+ public static double GetRelativeLuminance(Color color)
+ {
+ // The equation for relative luminance is given by
+ //
+ // L = 0.2126 * Rg + 0.7152 * Gg + 0.0722 * Bg
+ //
+ // where Xg = { X/3294 if X <= 10, (R/269 + 0.0513)^2.4 otherwise }
+ //
+ // If L is closer to 1, then the color is closer to white; if it is closer to 0,
+ // then the color is closer to black. This is based on the fact that the human
+ // eye perceives green to be much brighter than red, which in turn is perceived to be
+ // brighter than blue.
+
+ double rg = color.R <= 10 ? color.R / 3294.0 : Math.Pow(color.R / 269.0 + 0.0513, 2.4);
+ double gg = color.G <= 10 ? color.G / 3294.0 : Math.Pow(color.G / 269.0 + 0.0513, 2.4);
+ double bg = color.B <= 10 ? color.B / 3294.0 : Math.Pow(color.B / 269.0 + 0.0513, 2.4);
+
+ return (0.2126 * rg + 0.7152 * gg + 0.0722 * bg);
+ }
+ }
+}
diff --git a/src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.Properties.cs b/src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.Properties.cs
new file mode 100644
index 0000000000..824bf9ab05
--- /dev/null
+++ b/src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.Properties.cs
@@ -0,0 +1,207 @@
+// This source file is adapted from the WinUI project.
+// (https://github.com/microsoft/microsoft-ui-xaml)
+//
+// Licensed to The Avalonia Project under the MIT License.
+
+using Avalonia.Media;
+
+namespace Avalonia.Controls.Primitives
+{
+ ///
+ public partial class ColorSpectrum
+ {
+ ///
+ /// Gets or sets the currently selected color in the RGB color model.
+ ///
+ ///
+ /// For control authors use instead to avoid loss
+ /// of precision and color drifting.
+ ///
+ public Color Color
+ {
+ get => GetValue(ColorProperty);
+ set => SetValue(ColorProperty, value);
+ }
+
+ ///
+ /// Defines the property.
+ ///
+ public static readonly StyledProperty ColorProperty =
+ AvaloniaProperty.Register(
+ nameof(Color),
+ Color.FromArgb(0xFF, 0xFF, 0xFF, 0xFF));
+
+ ///
+ /// Gets or sets the two HSV color components displayed by the spectrum.
+ ///
+ ///
+ /// Internally, the uses the HSV color model.
+ ///
+ public ColorSpectrumComponents Components
+ {
+ get => GetValue(ComponentsProperty);
+ set => SetValue(ComponentsProperty, value);
+ }
+
+ ///
+ /// Defines the property.
+ ///
+ public static readonly StyledProperty ComponentsProperty =
+ AvaloniaProperty.Register(
+ nameof(Components),
+ ColorSpectrumComponents.HueSaturation);
+
+ ///
+ /// Gets or sets the currently selected color in the HSV color model.
+ ///
+ ///
+ /// This should be used in all cases instead of the property.
+ /// Internally, the uses the HSV color model and using
+ /// this property will avoid loss of precision and color drifting.
+ ///
+ public HsvColor HsvColor
+ {
+ get => GetValue(HsvColorProperty);
+ set => SetValue(HsvColorProperty, value);
+ }
+
+ ///
+ /// Defines the property.
+ ///
+ public static readonly StyledProperty HsvColorProperty =
+ AvaloniaProperty.Register(
+ nameof(HsvColor),
+ new HsvColor(1, 0, 0, 1));
+
+ ///
+ /// Gets or sets the maximum value of the Hue component in the range from 0..359.
+ /// This property must be greater than .
+ ///
+ ///
+ /// Internally, the uses the HSV color model.
+ ///
+ public int MaxHue
+ {
+ get => GetValue(MaxHueProperty);
+ set => SetValue(MaxHueProperty, value);
+ }
+
+ ///
+ /// Defines the property.
+ ///
+ public static readonly StyledProperty MaxHueProperty =
+ AvaloniaProperty.Register(nameof(MaxHue), 359);
+
+ ///
+ /// Gets or sets the maximum value of the Saturation component in the range from 0..100.
+ /// This property must be greater than .
+ ///
+ ///
+ /// Internally, the uses the HSV color model.
+ ///
+ public int MaxSaturation
+ {
+ get => GetValue(MaxSaturationProperty);
+ set => SetValue(MaxSaturationProperty, value);
+ }
+
+ ///
+ /// Defines the property.
+ ///
+ public static readonly StyledProperty MaxSaturationProperty =
+ AvaloniaProperty.Register(nameof(MaxSaturation), 100);
+
+ ///
+ /// Gets or sets the maximum value of the Value component in the range from 0..100.
+ /// This property must be greater than .
+ ///
+ ///
+ /// Internally, the uses the HSV color model.
+ ///
+ public int MaxValue
+ {
+ get => GetValue(MaxValueProperty);
+ set => SetValue(MaxValueProperty, value);
+ }
+
+ ///
+ /// Defines the property.
+ ///
+ public static readonly StyledProperty MaxValueProperty =
+ AvaloniaProperty.Register(nameof(MaxValue), 100);
+
+ ///
+ /// Gets or sets the minimum value of the Hue component in the range from 0..359.
+ /// This property must be less than .
+ ///
+ ///
+ /// Internally, the uses the HSV color model.
+ ///
+ public int MinHue
+ {
+ get => GetValue(MinHueProperty);
+ set => SetValue(MinHueProperty, value);
+ }
+
+ ///
+ /// Defines the property.
+ ///
+ public static readonly StyledProperty MinHueProperty =
+ AvaloniaProperty.Register(nameof(MinHue), 0);
+
+ ///
+ /// Gets or sets the minimum value of the Saturation component in the range from 0..100.
+ /// This property must be less than .
+ ///
+ ///
+ /// Internally, the uses the HSV color model.
+ ///
+ public int MinSaturation
+ {
+ get => GetValue(MinSaturationProperty);
+ set => SetValue(MinSaturationProperty, value);
+ }
+
+ ///
+ /// Defines the property.
+ ///
+ public static readonly StyledProperty MinSaturationProperty =
+ AvaloniaProperty.Register(nameof(MinSaturation), 0);
+
+ ///
+ /// Gets or sets the minimum value of the Value component in the range from 0..100.
+ /// This property must be less than .
+ ///
+ ///
+ /// Internally, the uses the HSV color model.
+ ///
+ public int MinValue
+ {
+ get => GetValue(MinValueProperty);
+ set => SetValue(MinValueProperty, value);
+ }
+
+ ///
+ /// Defines the property.
+ ///
+ public static readonly StyledProperty MinValueProperty =
+ AvaloniaProperty.Register(nameof(MinValue), 0);
+
+ ///
+ /// Gets or sets the displayed shape of the spectrum.
+ ///
+ public ColorSpectrumShape Shape
+ {
+ get => GetValue(ShapeProperty);
+ set => SetValue(ShapeProperty, value);
+ }
+
+ ///
+ /// Defines the property.
+ ///
+ public static readonly StyledProperty ShapeProperty =
+ AvaloniaProperty.Register(
+ nameof(Shape),
+ ColorSpectrumShape.Box);
+ }
+}
diff --git a/src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.cs b/src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.cs
new file mode 100644
index 0000000000..fe9a2fac43
--- /dev/null
+++ b/src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.cs
@@ -0,0 +1,1578 @@
+// This source file is adapted from the WinUI project.
+// (https://github.com/microsoft/microsoft-ui-xaml)
+//
+// Licensed to The Avalonia Project under the MIT License.
+
+using System;
+using System.Collections.Generic;
+using System.Threading.Tasks;
+using Avalonia.Controls.Metadata;
+using Avalonia.Controls.Shapes;
+using Avalonia.Input;
+using Avalonia.Interactivity;
+using Avalonia.Media;
+using Avalonia.Media.Imaging;
+using Avalonia.Threading;
+using Avalonia.Utilities;
+
+namespace Avalonia.Controls.Primitives
+{
+ ///
+ /// A two dimensional spectrum for color selection.
+ ///
+ [TemplatePart("PART_ColorNameToolTip", typeof(ToolTip))]
+ [TemplatePart("PART_InputTarget", typeof(Canvas))]
+ [TemplatePart("PART_LayoutRoot", typeof(Panel))]
+ [TemplatePart("PART_SelectionEllipsePanel", typeof(Panel))]
+ [TemplatePart("PART_SizingPanel", typeof(Panel))]
+ [TemplatePart("PART_SpectrumEllipse", typeof(Ellipse))]
+ [TemplatePart("PART_SpectrumRectangle", typeof(Rectangle))]
+ [TemplatePart("PART_SpectrumOverlayEllipse", typeof(Ellipse))]
+ [TemplatePart("PART_SpectrumOverlayRectangle", typeof(Rectangle))]
+ [PseudoClasses(pcPressed, pcLargeSelector, pcLightSelector)]
+ public partial class ColorSpectrum : TemplatedControl
+ {
+ protected const string pcPressed = ":pressed";
+ protected const string pcLargeSelector = ":large-selector";
+ protected const string pcLightSelector = ":light-selector";
+
+ ///
+ /// Event for when the selected color changes within the spectrum.
+ ///
+ public event EventHandler? ColorChanged;
+
+ private bool _updatingColor = false;
+ private bool _updatingHsvColor = false;
+ private bool _isPointerOver = false;
+ private bool _isPointerPressed = false;
+ private bool _shouldShowLargeSelection = false;
+ private List _hsvValues = new List();
+
+ private IDisposable? _layoutRootDisposable;
+ private IDisposable? _selectionEllipsePanelDisposable;
+
+ // XAML template parts
+ private Panel? _layoutRoot;
+ private Panel? _sizingPanel;
+ private Rectangle? _spectrumRectangle;
+ private Ellipse? _spectrumEllipse;
+ private Rectangle? _spectrumOverlayRectangle;
+ private Ellipse? _spectrumOverlayEllipse;
+ private Canvas? _inputTarget;
+ private Panel? _selectionEllipsePanel;
+ private ToolTip? _colorNameToolTip;
+
+ // Put the spectrum images in a bitmap, which is then given to an ImageBrush.
+ private WriteableBitmap? _hueRedBitmap;
+ private WriteableBitmap? _hueYellowBitmap;
+ private WriteableBitmap? _hueGreenBitmap;
+ private WriteableBitmap? _hueCyanBitmap;
+ private WriteableBitmap? _hueBlueBitmap;
+ private WriteableBitmap? _huePurpleBitmap;
+
+ private WriteableBitmap? _saturationMinimumBitmap;
+ private WriteableBitmap? _saturationMaximumBitmap;
+
+ private WriteableBitmap? _valueBitmap;
+
+ // Fields used by UpdateEllipse() to ensure that it's using the data
+ // associated with the last call to CreateBitmapsAndColorMap(),
+ // in order to function properly while the asynchronous bitmap creation
+ // is in progress.
+ private ColorSpectrumShape _shapeFromLastBitmapCreation = ColorSpectrumShape.Box;
+ private ColorSpectrumComponents _componentsFromLastBitmapCreation = ColorSpectrumComponents.HueSaturation;
+ private double _imageWidthFromLastBitmapCreation = 0.0;
+ private double _imageHeightFromLastBitmapCreation = 0.0;
+ private int _minHueFromLastBitmapCreation = 0;
+ private int _maxHueFromLastBitmapCreation = 0;
+ private int _minSaturationFromLastBitmapCreation = 0;
+ private int _maxSaturationFromLastBitmapCreation = 0;
+ private int _minValueFromLastBitmapCreation = 0;
+ private int _maxValueFromLastBitmapCreation = 0;
+
+ private Color _oldColor = Color.FromArgb(255, 255, 255, 255);
+ private HsvColor _oldHsvColor = HsvColor.FromAhsv(0.0f, 0.0f, 1.0f, 1.0f);
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public ColorSpectrum()
+ {
+ _shapeFromLastBitmapCreation = Shape;
+ _componentsFromLastBitmapCreation = Components;
+ _imageWidthFromLastBitmapCreation = 0;
+ _imageHeightFromLastBitmapCreation = 0;
+ _minHueFromLastBitmapCreation = MinHue;
+ _maxHueFromLastBitmapCreation = MaxHue;
+ _minSaturationFromLastBitmapCreation = MinSaturation;
+ _maxSaturationFromLastBitmapCreation = MaxSaturation;
+ _minValueFromLastBitmapCreation = MinValue;
+ _maxValueFromLastBitmapCreation = MaxValue;
+ }
+
+ ///
+ protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
+ {
+ base.OnApplyTemplate(e);
+
+ UnregisterEvents(); // Failsafe
+
+ _colorNameToolTip = e.NameScope.Find("PART_ColorNameToolTip");
+ _inputTarget = e.NameScope.Find
+
diff --git a/src/Avalonia.Themes.Fluent/Controls/FluentControls.xaml b/src/Avalonia.Themes.Fluent/Controls/FluentControls.xaml
index b36e0c7297..5b217e4764 100644
--- a/src/Avalonia.Themes.Fluent/Controls/FluentControls.xaml
+++ b/src/Avalonia.Themes.Fluent/Controls/FluentControls.xaml
@@ -61,8 +61,8 @@
-
-
+
+
diff --git a/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs b/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs
index 9d53691597..b1e4d8ca01 100644
--- a/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs
+++ b/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs
@@ -914,9 +914,9 @@ namespace Avalonia.Win32.Interop
[DllImport("user32.dll")]
public static extern bool EnumDisplayMonitors(IntPtr hdc, IntPtr lprcClip,
MonitorEnumDelegate lpfnEnum, IntPtr dwData);
-
+
public delegate bool MonitorEnumDelegate(IntPtr hMonitor, IntPtr hdcMonitor, ref Rect lprcMonitor, IntPtr dwData);
-
+
[DllImport("user32.dll", SetLastError = true)]
public static extern IntPtr GetDC(IntPtr hWnd);
@@ -1007,7 +1007,7 @@ namespace Avalonia.Win32.Interop
public static uint GetWindowLong(IntPtr hWnd, int nIndex)
{
- if(IntPtr.Size == 4)
+ if (IntPtr.Size == 4)
{
return GetWindowLong32b(hWnd, nIndex);
}
@@ -1034,7 +1034,7 @@ namespace Avalonia.Win32.Interop
return (uint)SetWindowLong64b(hWnd, nIndex, new IntPtr((uint)value)).ToInt32();
}
}
-
+
public static IntPtr SetWindowLongPtr(IntPtr hWnd, int nIndex, IntPtr handle)
{
if (IntPtr.Size == 4)
@@ -1068,14 +1068,14 @@ namespace Avalonia.Win32.Interop
[DllImport("user32.dll")]
public static extern bool InvalidateRect(IntPtr hWnd, RECT* lpRect, bool bErase);
-
-
+
+
[DllImport("user32.dll")]
public static extern bool ValidateRect(IntPtr hWnd, IntPtr lpRect);
[DllImport("user32.dll")]
public static extern bool IsWindow(IntPtr hWnd);
-
+
[DllImport("user32.dll")]
public static extern bool IsWindowEnabled(IntPtr hWnd);
@@ -1102,22 +1102,25 @@ namespace Avalonia.Win32.Interop
[DllImport("user32")]
public static extern IntPtr GetMessageExtraInfo();
-
+
[DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Unicode, EntryPoint = "RegisterClassExW")]
public static extern ushort RegisterClassEx(ref WNDCLASSEX lpwcx);
[DllImport("user32.dll")]
public static extern void RegisterTouchWindow(IntPtr hWnd, int flags);
-
+
[DllImport("user32.dll")]
public static extern bool ReleaseCapture();
+ [DllImport("user32.dll", SetLastError = true)]
+ public static extern uint RegisterWindowMessage(string lpString);
+
[DllImport("user32.dll")]
public static extern bool ScreenToClient(IntPtr hWnd, ref POINT lpPoint);
[DllImport("user32.dll", SetLastError = true)]
public static extern IntPtr GetActiveWindow();
-
+
[DllImport("user32.dll", SetLastError = true)]
public static extern IntPtr SetActiveWindow(IntPtr hWnd);
@@ -1304,7 +1307,7 @@ namespace Avalonia.Win32.Interop
[DllImport("kernel32.dll", SetLastError = true)]
public static extern IntPtr LoadLibrary(string fileName);
-
+
[DllImport("kernel32.dll", SetLastError = true)]
public static extern IntPtr LoadLibraryEx(string fileName, IntPtr hFile, int flags);
@@ -1348,7 +1351,7 @@ namespace Avalonia.Win32.Interop
[DllImport("user32.dll")]
public static extern IntPtr MonitorFromWindow(IntPtr hwnd, MONITOR dwFlags);
-
+
[DllImport("user32", EntryPoint = "GetMonitorInfoW", ExactSpelling = true, CharSet = CharSet.Unicode)]
[return: MarshalAs(UnmanagedType.Bool)]
public static extern bool GetMonitorInfo([In] IntPtr hMonitor, ref MONITORINFO lpmi);
@@ -1356,14 +1359,14 @@ namespace Avalonia.Win32.Interop
[DllImport("user32")]
public static extern unsafe bool GetTouchInputInfo(
IntPtr hTouchInput,
- uint cInputs,
+ uint cInputs,
TOUCHINPUT* pInputs,
- int cbSize
+ int cbSize
);
-
+
[DllImport("user32")]
public static extern bool CloseTouchInputHandle(IntPtr hTouchInput);
-
+
[return: MarshalAs(UnmanagedType.Bool)]
[DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Unicode, EntryPoint = "PostMessageW")]
public static extern bool PostMessage(IntPtr hWnd, uint Msg, IntPtr wParam, IntPtr lParam);
@@ -1372,7 +1375,7 @@ namespace Avalonia.Win32.Interop
public static extern int SetDIBitsToDevice(IntPtr hdc, int XDest, int YDest, uint
dwWidth, uint dwHeight, int XSrc, int YSrc, uint uStartScan, uint cScanLines,
IntPtr lpvBits, [In] ref BITMAPINFOHEADER lpbmi, uint fuColorUse);
-
+
[DllImport("kernel32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
public static extern bool CloseHandle(IntPtr hObject);
@@ -1387,27 +1390,27 @@ namespace Avalonia.Win32.Interop
[DllImport("gdi32.dll")]
public static extern int ChoosePixelFormat(IntPtr hdc, ref PixelFormatDescriptor pfd);
-
+
[DllImport("gdi32.dll")]
public static extern int DescribePixelFormat(IntPtr hdc, ref PixelFormatDescriptor pfd);
[DllImport("gdi32.dll")]
public static extern int SetPixelFormat(IntPtr hdc, int iPixelFormat, ref PixelFormatDescriptor pfd);
-
-
+
+
[DllImport("gdi32.dll")]
public static extern int DescribePixelFormat(IntPtr hdc, int iPixelFormat, int bytes, ref PixelFormatDescriptor pfd);
-
+
[DllImport("gdi32.dll")]
public static extern bool SwapBuffers(IntPtr hdc);
[DllImport("opengl32.dll")]
public static extern IntPtr wglCreateContext(IntPtr hdc);
-
+
[DllImport("opengl32.dll")]
public static extern bool wglDeleteContext(IntPtr context);
-
+
[DllImport("opengl32.dll")]
public static extern bool wglMakeCurrent(IntPtr hdc, IntPtr context);
@@ -1428,9 +1431,9 @@ namespace Avalonia.Win32.Interop
uint dwMaximumSizeLow,
string lpName);
- [DllImport("msvcrt.dll", EntryPoint="memcpy", SetLastError = false, CallingConvention=CallingConvention.Cdecl)]
- public static extern IntPtr CopyMemory(IntPtr dest, IntPtr src, UIntPtr count);
-
+ [DllImport("msvcrt.dll", EntryPoint = "memcpy", SetLastError = false, CallingConvention = CallingConvention.Cdecl)]
+ public static extern IntPtr CopyMemory(IntPtr dest, IntPtr src, UIntPtr count);
+
[DllImport("ole32.dll", CharSet = CharSet.Auto, ExactSpelling = true)]
public static extern HRESULT RegisterDragDrop(IntPtr hwnd, IntPtr target);
@@ -1472,10 +1475,10 @@ namespace Avalonia.Win32.Interop
[DllImport("dwmapi.dll")]
public static extern void DwmFlush();
-
+
[DllImport("dwmapi.dll")]
public static extern bool DwmDefWindowProc(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam, ref IntPtr plResult);
-
+
[DllImport("dwmapi.dll")]
public static extern void DwmEnableBlurBehindWindow(IntPtr hwnd, ref DWM_BLURBEHIND blurBehind);
@@ -1542,8 +1545,8 @@ namespace Avalonia.Win32.Interop
throw new Exception("RtlGetVersion failed!");
}
}
-
- [DllImport("kernel32", EntryPoint="WaitForMultipleObjectsEx", SetLastError = true, CharSet = CharSet.Auto)]
+
+ [DllImport("kernel32", EntryPoint = "WaitForMultipleObjectsEx", SetLastError = true, CharSet = CharSet.Auto)]
private static extern int IntWaitForMultipleObjectsEx(int nCount, IntPtr[] pHandles, bool bWaitAll, int dwMilliseconds, bool bAlertable);
public const int WAIT_FAILED = unchecked((int)0xFFFFFFFF);
@@ -1551,7 +1554,7 @@ namespace Avalonia.Win32.Interop
internal static int WaitForMultipleObjectsEx(int nCount, IntPtr[] pHandles, bool bWaitAll, int dwMilliseconds, bool bAlertable)
{
int result = IntWaitForMultipleObjectsEx(nCount, pHandles, bWaitAll, dwMilliseconds, bAlertable);
- if(result == WAIT_FAILED)
+ if (result == WAIT_FAILED)
{
throw new Win32Exception();
}
@@ -1699,7 +1702,7 @@ namespace Avalonia.Win32.Interop
DrawLeftBorder = 0x20,
DrawTopBorder = 0x40,
DrawRightBorder = 0x80,
- DrawBottomBorder = 0x100,
+ DrawBottomBorder = 0x100,
}
[StructLayout(LayoutKind.Sequential)]
@@ -1767,9 +1770,9 @@ namespace Avalonia.Win32.Interop
MDT_ANGULAR_DPI = 1,
MDT_RAW_DPI = 2,
MDT_DEFAULT = MDT_EFFECTIVE_DPI
- }
+ }
- public enum ClipboardFormat
+ public enum ClipboardFormat
{
///
/// Text format. Each line ends with a carriage return/linefeed (CR-LF) combination. A null character signals the end of the data. Use this format for ANSI text.
@@ -1820,7 +1823,7 @@ namespace Avalonia.Win32.Interop
public int X;
public int Y;
}
-
+
public struct SIZE
{
public int X;
@@ -2021,7 +2024,7 @@ namespace Avalonia.Win32.Interop
OFN_NOREADONLYRETURN = 0x00008000,
OFN_OVERWRITEPROMPT = 0x00000002
}
-
+
public enum HRESULT : uint
{
S_FALSE = 0x0001,
diff --git a/src/Windows/Avalonia.Win32/TrayIconImpl.cs b/src/Windows/Avalonia.Win32/TrayIconImpl.cs
index 6484ae6c54..771ab232da 100644
--- a/src/Windows/Avalonia.Win32/TrayIconImpl.cs
+++ b/src/Windows/Avalonia.Win32/TrayIconImpl.cs
@@ -24,6 +24,7 @@ namespace Avalonia.Win32
private readonly Win32NativeToManagedMenuExporter _exporter;
private static readonly Dictionary s_trayIcons = new Dictionary();
private bool _disposedValue;
+ private static readonly uint WM_TASKBARCREATED = UnmanagedMethods.RegisterWindowMessage("TaskbarCreated");
public TrayIconImpl()
{
@@ -44,6 +45,18 @@ namespace Avalonia.Win32
{
s_trayIcons[wParam.ToInt32()].WndProc(hWnd, msg, wParam, lParam);
}
+
+ if (msg == WM_TASKBARCREATED)
+ {
+ foreach (var tray in s_trayIcons.Values)
+ {
+ if (tray._iconAdded)
+ {
+ tray.UpdateIcon(true);
+ tray.UpdateIcon();
+ }
+ }
+ }
}
public void SetIcon(IWindowIconImpl? icon)
@@ -145,7 +158,7 @@ namespace Avalonia.Win32
private enum CustomWindowsMessage : uint
{
WM_TRAYICON = WindowsMessage.WM_APP + 1024,
- WM_TRAYMOUSE = WindowsMessage.WM_USER + 1024
+ WM_TRAYMOUSE = WindowsMessage.WM_USER + 1024,
}
private class TrayIconMenuFlyoutPresenter : MenuFlyoutPresenter, IStyleable
diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs
index 0e0ca7cd25..3d36395c3a 100644
--- a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs
+++ b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs
@@ -6,6 +6,7 @@ using System.Collections.Specialized;
using System.ComponentModel;
using System.Linq;
using System.Reactive.Disposables;
+using System.Threading.Tasks;
using Avalonia.Collections;
using Avalonia.Controls.Presenters;
using Avalonia.Controls.Primitives;
@@ -1614,6 +1615,50 @@ namespace Avalonia.Controls.UnitTests.Primitives
target.MoveSelection(NavigationDirection.Next, true);
}
+ [Fact(Timeout = 2000)]
+ public async Task MoveSelection_Does_Not_Hang_With_No_Focusable_Controls_And_Moving_Selection_To_The_First_Item()
+ {
+ var target = new TestSelector
+ {
+ Template = Template(),
+ Items = new[]
+ {
+ new ListBoxItem { Focusable = false },
+ new ListBoxItem(),
+ }
+ };
+
+ target.Measure(new Size(100, 100));
+ target.Arrange(new Rect(0, 0, 100, 100));
+
+ // Timeout in xUnit doesen't work with synchronous methods so we need to apply hack below.
+ // https://github.com/xunit/xunit/issues/2222
+ await Task.Run(() => target.MoveSelection(NavigationDirection.First, true));
+ Assert.Equal(-1, target.SelectedIndex);
+ }
+
+ [Fact(Timeout = 2000)]
+ public async Task MoveSelection_Does_Not_Hang_With_No_Focusable_Controls_And_Moving_Selection_To_The_Last_Item()
+ {
+ var target = new TestSelector
+ {
+ Template = Template(),
+ Items = new[]
+ {
+ new ListBoxItem(),
+ new ListBoxItem { Focusable = false },
+ }
+ };
+
+ target.Measure(new Size(100, 100));
+ target.Arrange(new Rect(0, 0, 100, 100));
+
+ // Timeout in xUnit doesen't work with synchronous methods so we need to apply hack below.
+ // https://github.com/xunit/xunit/issues/2222
+ await Task.Run(() => target.MoveSelection(NavigationDirection.Last, true));
+ Assert.Equal(-1, target.SelectedIndex);
+ }
+
[Fact]
public void MoveSelection_Does_Select_Disabled_Controls()
{
diff --git a/tests/Avalonia.LeakTests/Avalonia.LeakTests.csproj b/tests/Avalonia.LeakTests/Avalonia.LeakTests.csproj
index 0c663e1a8f..a00b24bdd7 100644
--- a/tests/Avalonia.LeakTests/Avalonia.LeakTests.csproj
+++ b/tests/Avalonia.LeakTests/Avalonia.LeakTests.csproj
@@ -9,6 +9,7 @@
+