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..7ebb87094a 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/Fluent.xaml")
+ };
+
+ public static readonly StyleInclude ColorPickerDefault = new StyleInclude(new Uri("avares://ControlCatalog/Styles"))
+ {
+ Source = new Uri("avares://Avalonia.Controls.ColorPicker/Themes/Default/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..c0c83d6a35
--- /dev/null
+++ b/samples/ControlCatalog/Pages/ColorPickerPage.xaml
@@ -0,0 +1,79 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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/Controls/ResourceNodeExtensions.cs b/src/Avalonia.Base/Controls/ResourceNodeExtensions.cs
index 513b3f2424..1758c45650 100644
--- a/src/Avalonia.Base/Controls/ResourceNodeExtensions.cs
+++ b/src/Avalonia.Base/Controls/ResourceNodeExtensions.cs
@@ -40,19 +40,16 @@ namespace Avalonia.Controls
control = control ?? throw new ArgumentNullException(nameof(control));
key = key ?? throw new ArgumentNullException(nameof(key));
- IResourceHost? current = control;
+ IResourceNode? current = control;
while (current != null)
{
- if (current is IResourceHost host)
+ if (current.TryGetResource(key, out value))
{
- if (host.TryGetResource(key, out value))
- {
- return true;
- }
+ return true;
}
- current = (current as IStyledElement)?.StylingParent as IResourceHost;
+ current = (current as IStyledElement)?.StylingParent as IResourceNode;
}
value = null;
diff --git a/src/Avalonia.Base/Media/GlyphRun.cs b/src/Avalonia.Base/Media/GlyphRun.cs
index ec270d796a..22be8d8865 100644
--- a/src/Avalonia.Base/Media/GlyphRun.cs
+++ b/src/Avalonia.Base/Media/GlyphRun.cs
@@ -28,6 +28,8 @@ namespace Avalonia.Media
private IReadOnlyList? _glyphOffsets;
private IReadOnlyList? _glyphClusters;
+ private int _offsetToFirstCharacter;
+
///
/// Initializes a new instance of the class by specifying properties of the class.
///
@@ -49,7 +51,7 @@ namespace Avalonia.Media
IReadOnlyList? glyphClusters = null,
int biDiLevel = 0)
{
- _glyphTypeface = glyphTypeface;
+ _glyphTypeface = glyphTypeface;
FontRenderingEmSize = fontRenderingEmSize;
@@ -203,8 +205,8 @@ namespace Avalonia.Media
///
public double GetDistanceFromCharacterHit(CharacterHit characterHit)
{
- var characterIndex = characterHit.FirstCharacterIndex + characterHit.TrailingLength;
-
+ var characterIndex = characterHit.FirstCharacterIndex + characterHit.TrailingLength - _offsetToFirstCharacter;
+
var distance = 0.0;
if (IsLeftToRight)
@@ -223,7 +225,7 @@ namespace Avalonia.Media
}
var glyphIndex = FindGlyphIndex(characterIndex);
-
+
if (GlyphClusters != null)
{
var currentCluster = GlyphClusters[glyphIndex];
@@ -249,7 +251,7 @@ namespace Avalonia.Media
{
//RightToLeft
var glyphIndex = FindGlyphIndex(characterIndex);
-
+
if (GlyphClusters != null)
{
if (characterIndex > GlyphClusters[0])
@@ -284,13 +286,13 @@ namespace Avalonia.Media
public CharacterHit GetCharacterHitFromDistance(double distance, out bool isInside)
{
var characterIndex = 0;
-
+
// Before
if (distance <= 0)
{
isInside = false;
- if(GlyphClusters != null)
+ if (GlyphClusters != null)
{
characterIndex = GlyphClusters[characterIndex];
}
@@ -307,11 +309,11 @@ namespace Avalonia.Media
characterIndex = GlyphIndices.Count - 1;
- if(GlyphClusters != null)
+ if (GlyphClusters != null)
{
characterIndex = GlyphClusters[characterIndex];
}
-
+
var lastCharacterHit = FindNearestCharacterHit(characterIndex, out _);
return IsLeftToRight ? lastCharacterHit : new CharacterHit(lastCharacterHit.FirstCharacterIndex);
@@ -327,7 +329,7 @@ namespace Avalonia.Media
var advance = GetGlyphAdvance(index, out var cluster);
characterIndex = cluster;
-
+
if (distance > currentX && distance <= currentX + advance)
{
break;
@@ -345,7 +347,7 @@ namespace Avalonia.Media
var advance = GetGlyphAdvance(index, out var cluster);
characterIndex = cluster;
-
+
if (currentX - advance < distance)
{
break;
@@ -552,20 +554,20 @@ namespace Avalonia.Media
}
nextCluster = GlyphClusters[currentIndex];
- }
+ }
int trailingLength;
if (nextCluster == cluster)
{
- trailingLength = Characters.Start + Characters.Length - cluster;
+ trailingLength = Characters.Start + Characters.Length - _offsetToFirstCharacter - cluster;
}
else
{
trailingLength = nextCluster - cluster;
}
- return new CharacterHit(cluster, trailingLength);
+ return new CharacterHit(_offsetToFirstCharacter + cluster, trailingLength);
}
///
@@ -577,7 +579,7 @@ namespace Avalonia.Media
private double GetGlyphAdvance(int index, out int cluster)
{
cluster = GlyphClusters != null ? GlyphClusters[index] : index;
-
+
if (GlyphAdvances != null)
{
return GlyphAdvances[index];
@@ -599,11 +601,18 @@ namespace Avalonia.Media
private GlyphRunMetrics CreateGlyphRunMetrics()
{
+ if (GlyphClusters != null && GlyphClusters.Count > 0)
+ {
+ var firstCluster = GlyphClusters[0];
+
+ _offsetToFirstCharacter = Math.Max(0, Characters.Start - firstCluster);
+ }
+
var height = (GlyphTypeface.Descent - GlyphTypeface.Ascent + GlyphTypeface.LineGap) * Scale;
var widthIncludingTrailingWhitespace = 0d;
var trailingWhitespaceLength = GetTrailingWhitespaceLength(out var newLineLength, out var glyphCount);
-
+
for (var index = 0; index < GlyphIndices.Count; index++)
{
var advance = GetGlyphAdvance(index, out _);
@@ -615,7 +624,7 @@ namespace Avalonia.Media
if (IsLeftToRight)
{
- for (var index = GlyphIndices.Count - glyphCount; index = 0; i--)
{
- var cluster = GlyphClusters[i];
+ var currentCluster = GlyphClusters[i];
+ var characterIndex = Math.Max(0, currentCluster - _characters.BufferOffset);
+ var codepoint = Codepoint.ReadAt(_characters, characterIndex, out _);
- var codepointIndex = IsLeftToRight ? cluster - _characters.Start : _characters.End - cluster;
-
- if (codepointIndex < 0)
+ if (!codepoint.IsWhiteSpace)
{
- trailingWhitespaceLength = _characters.Length;
-
- glyphCount = GlyphClusters.Count;
-
break;
}
-
- var codepoint = Codepoint.ReadAt(_characters, codepointIndex, out _);
- if (!codepoint.IsWhiteSpace)
+ var clusterLength = 1;
+
+ while(i - 1 >= 0)
{
+ var nextCluster = GlyphClusters[i - 1];
+
+ if(currentCluster == nextCluster)
+ {
+ clusterLength++;
+ i--;
+
+ continue;
+ }
+
break;
}
if (codepoint.IsBreakChar)
{
- newLineLength++;
+ newLineLength += clusterLength;
}
- trailingWhitespaceLength++;
-
- glyphCount++;
+ trailingWhitespaceLength += clusterLength;
+
+ glyphCount++;
}
}
diff --git a/src/Avalonia.Base/Media/TextFormatting/TextFormatter.cs b/src/Avalonia.Base/Media/TextFormatting/TextFormatter.cs
index d521077a43..0b5d7649d7 100644
--- a/src/Avalonia.Base/Media/TextFormatting/TextFormatter.cs
+++ b/src/Avalonia.Base/Media/TextFormatting/TextFormatter.cs
@@ -1,6 +1,4 @@
-using Avalonia.Media.TextFormatting.Unicode;
-
-namespace Avalonia.Media.TextFormatting
+namespace Avalonia.Media.TextFormatting
{
///
/// Represents a base class for text formatting.
diff --git a/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs b/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs
index 7c60f73b8d..7f0f204886 100644
--- a/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs
+++ b/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs
@@ -8,6 +8,8 @@ namespace Avalonia.Media.TextFormatting
{
internal class TextFormatterImpl : TextFormatter
{
+ private static readonly char[] s_empty = { ' ' };
+
///
public override TextLine FormatLine(ITextSource textSource, int firstTextSourceIndex, double paragraphWidth,
TextParagraphProperties paragraphProperties, TextLineBreak? previousLineBreak = null)
@@ -77,14 +79,14 @@ namespace Avalonia.Media.TextFormatting
{
var currentRun = textRuns[i];
- if (currentLength + currentRun.Text.Length < length)
+ if (currentLength + currentRun.TextSourceLength < length)
{
currentLength += currentRun.TextSourceLength;
continue;
}
- var firstCount = currentRun.Text.Length >= 1 ? i + 1 : i;
+ var firstCount = currentRun.TextSourceLength >= 1 ? i + 1 : i;
var first = new List(firstCount);
@@ -98,13 +100,13 @@ namespace Avalonia.Media.TextFormatting
var secondCount = textRuns.Count - firstCount;
- if (currentLength + currentRun.Text.Length == length)
+ if (currentLength + currentRun.TextSourceLength == length)
{
var second = secondCount > 0 ? new List(secondCount) : null;
if (second != null)
{
- var offset = currentRun.Text.Length >= 1 ? 1 : 0;
+ var offset = currentRun.TextSourceLength >= 1 ? 1 : 0;
for (var j = 0; j < secondCount; j++)
{
@@ -122,16 +124,14 @@ namespace Avalonia.Media.TextFormatting
var second = new List(secondCount);
- if (currentRun is not ShapedTextCharacters shapedTextCharacters)
+ if (currentRun is ShapedTextCharacters shapedTextCharacters)
{
- throw new NotSupportedException("Only shaped runs can be split in between.");
- }
-
- var split = shapedTextCharacters.Split(length - currentLength);
+ var split = shapedTextCharacters.Split(length - currentLength);
- first.Add(split.First);
+ first.Add(split.First);
- second.Add(split.Second!);
+ second.Add(split.Second!);
+ }
for (var j = 1; j < secondCount; j++)
{
@@ -267,7 +267,6 @@ namespace Avalonia.Media.TextFormatting
IReadOnlyList textRuns, ReadOnlySlice text, TextShaperOptions options)
{
var shapedRuns = new List(textRuns.Count);
- var firstRun = textRuns[0];
var shapedBuffer = TextShaper.Current.ShapeText(text, options);
@@ -471,11 +470,10 @@ namespace Avalonia.Media.TextFormatting
return false;
}
- private static bool TryMeasureLength(IReadOnlyList textRuns, int firstTextSourceIndex, double paragraphWidth, out int measuredLength)
+ private static bool TryMeasureLength(IReadOnlyList textRuns, double paragraphWidth, out int measuredLength)
{
measuredLength = 0;
var currentWidth = 0.0;
- var lastCluster = firstTextSourceIndex;
foreach (var currentRun in textRuns)
{
@@ -483,12 +481,17 @@ namespace Avalonia.Media.TextFormatting
{
case ShapedTextCharacters shapedTextCharacters:
{
+ var firstCluster = shapedTextCharacters.ShapedBuffer.GlyphClusters[0];
+ var lastCluster = firstCluster;
+
for (var i = 0; i < shapedTextCharacters.ShapedBuffer.Length; i++)
{
var glyphInfo = shapedTextCharacters.ShapedBuffer[i];
if (currentWidth + glyphInfo.GlyphAdvance > paragraphWidth)
{
+ measuredLength += Math.Max(0, lastCluster - firstCluster);
+
goto found;
}
@@ -496,6 +499,8 @@ namespace Avalonia.Media.TextFormatting
currentWidth += glyphInfo.GlyphAdvance;
}
+ measuredLength += currentRun.TextSourceLength;
+
break;
}
@@ -506,7 +511,7 @@ namespace Avalonia.Media.TextFormatting
goto found;
}
- lastCluster += currentRun.TextSourceLength;
+ measuredLength += currentRun.TextSourceLength;
currentWidth += currentRun.Size.Width;
break;
@@ -516,11 +521,30 @@ namespace Avalonia.Media.TextFormatting
found:
- measuredLength = Math.Max(0, lastCluster - firstTextSourceIndex + 1);
-
return measuredLength != 0;
}
+ ///
+ /// Creates an empty text line.
+ ///
+ /// The empty text line.
+ public static TextLineImpl CreateEmptyTextLine(int firstTextSourceIndex, TextParagraphProperties paragraphProperties)
+ {
+ var flowDirection = paragraphProperties.FlowDirection;
+ var properties = paragraphProperties.DefaultTextRunProperties;
+ var glyphTypeface = properties.Typeface.GlyphTypeface;
+ var text = new ReadOnlySlice(s_empty, firstTextSourceIndex, 1);
+ var glyph = glyphTypeface.GetGlyph(s_empty[0]);
+ var glyphInfos = new[] { new GlyphInfo(glyph, firstTextSourceIndex) };
+
+ var shapedBuffer = new ShapedBuffer(text, glyphInfos, glyphTypeface, properties.FontRenderingEmSize,
+ (sbyte)flowDirection);
+
+ var textRuns = new List { new ShapedTextCharacters(shapedBuffer, properties) };
+
+ return new TextLineImpl(textRuns, firstTextSourceIndex, 0, double.PositiveInfinity, paragraphProperties, flowDirection).FinalizeLine();
+ }
+
///
/// Performs text wrapping returns a list of text lines.
///
@@ -535,7 +559,12 @@ namespace Avalonia.Media.TextFormatting
double paragraphWidth, TextParagraphProperties paragraphProperties, FlowDirection flowDirection,
TextLineBreak? currentLineBreak)
{
- if (!TryMeasureLength(textRuns, firstTextSourceIndex, paragraphWidth, out var measuredLength))
+ if(textRuns.Count == 0)
+ {
+ return CreateEmptyTextLine(firstTextSourceIndex, paragraphProperties);
+ }
+
+ if (!TryMeasureLength(textRuns, paragraphWidth, out var measuredLength))
{
measuredLength = 1;
}
diff --git a/src/Avalonia.Base/Media/TextFormatting/TextLayout.cs b/src/Avalonia.Base/Media/TextFormatting/TextLayout.cs
index e3bcdee014..0df608cb34 100644
--- a/src/Avalonia.Base/Media/TextFormatting/TextLayout.cs
+++ b/src/Avalonia.Base/Media/TextFormatting/TextLayout.cs
@@ -10,13 +10,12 @@ namespace Avalonia.Media.TextFormatting
///
public class TextLayout
{
- private static readonly char[] s_empty = { ' ' };
-
- private readonly ReadOnlySlice _text;
+ private readonly ITextSource _textSource;
private readonly TextParagraphProperties _paragraphProperties;
- private readonly IReadOnlyList>? _textStyleOverrides;
private readonly TextTrimming _textTrimming;
+ private int _textSourceLength;
+
///
/// Initializes a new instance of the class.
///
@@ -50,17 +49,49 @@ namespace Avalonia.Media.TextFormatting
int maxLines = 0,
IReadOnlyList>? textStyleOverrides = null)
{
- _text = string.IsNullOrEmpty(text) ?
- new ReadOnlySlice() :
- new ReadOnlySlice(text.AsMemory());
-
_paragraphProperties =
CreateTextParagraphProperties(typeface, fontSize, foreground, textAlignment, textWrapping,
textDecorations, flowDirection, lineHeight);
+ _textSource = new FormattedTextSource(text.AsMemory(), _paragraphProperties.DefaultTextRunProperties, textStyleOverrides);
+
_textTrimming = textTrimming ?? TextTrimming.None;
- _textStyleOverrides = textStyleOverrides;
+ LineHeight = lineHeight;
+
+ MaxWidth = maxWidth;
+
+ MaxHeight = maxHeight;
+
+ MaxLines = maxLines;
+
+ TextLines = CreateTextLines();
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The text source.
+ /// The default text paragraph properties.
+ /// The text trimming.
+ /// The maximum width.
+ /// The maximum height.
+ /// The height of each line of text.
+ /// The maximum number of text lines.
+ public TextLayout(
+ ITextSource textSource,
+ TextParagraphProperties paragraphProperties,
+ TextTrimming? textTrimming = null,
+ double maxWidth = double.PositiveInfinity,
+ double maxHeight = double.PositiveInfinity,
+ double lineHeight = double.NaN,
+ int maxLines = 0)
+ {
+ _textSource = textSource;
+
+ _paragraphProperties = paragraphProperties;
+
+ _textTrimming = textTrimming ?? TextTrimming.None;
LineHeight = lineHeight;
@@ -147,7 +178,7 @@ namespace Avalonia.Media.TextFormatting
return new Rect();
}
- if (textPosition < 0 || textPosition >= _text.Length)
+ if (textPosition < 0 || textPosition >= _textSourceLength)
{
var lastLine = TextLines[TextLines.Count - 1];
@@ -273,7 +304,7 @@ namespace Avalonia.Media.TextFormatting
return 0;
}
- if (charIndex > _text.Length)
+ if (charIndex > _textSourceLength)
{
return TextLines.Count - 1;
}
@@ -375,32 +406,11 @@ namespace Avalonia.Media.TextFormatting
height += textLine.Height;
}
- ///
- /// Creates an empty text line.
- ///
- /// The empty text line.
- private TextLine CreateEmptyTextLine(int firstTextSourceIndex)
- {
- var flowDirection = _paragraphProperties.FlowDirection;
- var properties = _paragraphProperties.DefaultTextRunProperties;
- var glyphTypeface = properties.Typeface.GlyphTypeface;
- var text = new ReadOnlySlice(s_empty, firstTextSourceIndex, 1);
- var glyph = glyphTypeface.GetGlyph(s_empty[0]);
- var glyphInfos = new[] { new GlyphInfo(glyph, firstTextSourceIndex) };
-
- var shapedBuffer = new ShapedBuffer(text, glyphInfos, glyphTypeface, properties.FontRenderingEmSize,
- (sbyte)flowDirection);
-
- var textRuns = new List { new ShapedTextCharacters(shapedBuffer, properties) };
-
- return new TextLineImpl(textRuns, firstTextSourceIndex, 1, MaxWidth, _paragraphProperties, flowDirection).FinalizeLine();
- }
-
private IReadOnlyList CreateTextLines()
{
- if (_text.IsEmpty || MathUtilities.IsZero(MaxWidth) || MathUtilities.IsZero(MaxHeight))
+ if (MathUtilities.IsZero(MaxWidth) || MathUtilities.IsZero(MaxHeight))
{
- var textLine = CreateEmptyTextLine(0);
+ var textLine = TextFormatterImpl.CreateEmptyTextLine(0, _paragraphProperties);
Bounds = new Rect(0,0,0, textLine.Height);
@@ -411,26 +421,30 @@ namespace Avalonia.Media.TextFormatting
double left = double.PositiveInfinity, width = 0.0, height = 0.0;
- var currentPosition = 0;
-
- var textSource = new FormattedTextSource(_text,
- _paragraphProperties.DefaultTextRunProperties, _textStyleOverrides);
+ _textSourceLength = 0;
TextLine? previousLine = null;
- while (currentPosition < _text.Length)
+ while (true)
{
- var textLine = TextFormatter.Current.FormatLine(textSource, currentPosition, MaxWidth,
+ var textLine = TextFormatter.Current.FormatLine(_textSource, _textSourceLength, MaxWidth,
_paragraphProperties, previousLine?.TextLineBreak);
-#if DEBUG
- if (textLine.Length == 0)
+ if(textLine == null || textLine.Length == 0)
{
- throw new InvalidOperationException($"{nameof(textLine)} should not be empty.");
+ if(previousLine != null && previousLine.NewLineLength > 0)
+ {
+ var emptyTextLine = TextFormatterImpl.CreateEmptyTextLine(_textSourceLength, _paragraphProperties);
+
+ textLines.Add(emptyTextLine);
+
+ UpdateBounds(emptyTextLine, ref left, ref width, ref height);
+ }
+
+ break;
}
-#endif
- currentPosition += textLine.Length;
+ _textSourceLength += textLine.Length;
//Fulfill max height constraint
if (textLines.Count > 0 && !double.IsPositiveInfinity(MaxHeight) && height + textLine.Height > MaxHeight)
@@ -464,17 +478,16 @@ namespace Avalonia.Media.TextFormatting
{
break;
}
-
- if (currentPosition != _text.Length || textLine.NewLineLength <= 0)
- {
- continue;
- }
+ }
- var emptyTextLine = CreateEmptyTextLine(currentPosition);
+ //Make sure the TextLayout always contains at least on empty line
+ if(textLines.Count == 0)
+ {
+ var textLine = TextFormatterImpl.CreateEmptyTextLine(0, _paragraphProperties);
- textLines.Add(emptyTextLine);
+ textLines.Add(textLine);
- UpdateBounds(emptyTextLine,ref left, ref width, ref height);
+ UpdateBounds(textLine, ref left, ref width, ref height);
}
Bounds = new Rect(left, 0, width, height);
diff --git a/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs b/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs
index 30e3728d1f..6a704f6f3e 100644
--- a/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs
+++ b/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs
@@ -183,6 +183,7 @@ namespace Avalonia.Media.TextFormatting
case ShapedTextCharacters shapedRun:
{
characterHit = shapedRun.GlyphRun.GetCharacterHitFromDistance(distance, out _);
+
break;
}
default:
@@ -403,7 +404,7 @@ namespace Avalonia.Media.TextFormatting
var result = new List(TextRuns.Count);
var lastDirection = _flowDirection;
var currentDirection = lastDirection;
- var currentPosition = 0;
+ var currentPosition = FirstTextSourceIndex;
var currentRect = Rect.Empty;
var startX = Start;
@@ -417,6 +418,11 @@ namespace Avalonia.Media.TextFormatting
continue;
}
+ if(currentPosition + currentRun.TextSourceLength <= firstTextSourceCharacterIndex)
+ {
+ continue;
+ }
+
TextRun? nextRun = null;
if (index + 1 < TextRuns.Count)
@@ -426,31 +432,42 @@ namespace Avalonia.Media.TextFormatting
if (nextRun != null)
{
- if (nextRun.Text.Start < currentRun.Text.Start && firstTextSourceCharacterIndex + textLength < currentRun.Text.End)
+ switch (nextRun)
{
- goto skip;
- }
+ case ShapedTextCharacters when currentRun is ShapedTextCharacters:
+ {
+ if (nextRun.Text.Start < currentRun.Text.Start && firstTextSourceCharacterIndex + textLength < currentRun.Text.End)
+ {
+ goto skip;
+ }
- if (currentRun.Text.Start >= firstTextSourceCharacterIndex + textLength)
- {
- goto skip;
- }
+ if (currentRun.Text.Start >= firstTextSourceCharacterIndex + textLength)
+ {
+ goto skip;
+ }
- if (currentRun.Text.Start > nextRun.Text.Start && currentRun.Text.Start < firstTextSourceCharacterIndex)
- {
- goto skip;
- }
+ if (currentRun.Text.Start > nextRun.Text.Start && currentRun.Text.Start < firstTextSourceCharacterIndex)
+ {
+ goto skip;
+ }
- if (currentRun.Text.End < firstTextSourceCharacterIndex)
- {
- goto skip;
- }
+ if (currentRun.Text.End < firstTextSourceCharacterIndex)
+ {
+ goto skip;
+ }
- goto noop;
+ goto noop;
+ }
+ default:
+ {
+ goto noop;
+ }
+ }
skip:
{
startX += currentRun.Size.Width;
+ currentPosition += currentRun.TextSourceLength;
}
continue;
@@ -460,7 +477,6 @@ namespace Avalonia.Media.TextFormatting
}
}
-
var endX = startX;
var endOffset = 0d;
@@ -520,11 +536,13 @@ namespace Avalonia.Media.TextFormatting
}
default:
{
- if (firstTextSourceCharacterIndex + textLength >= currentRun.Text.Start + currentRun.Text.Length)
+ if (currentPosition + currentRun.TextSourceLength <= firstTextSourceCharacterIndex + textLength)
{
endX += currentRun.Size.Width;
}
+ currentPosition += currentRun.TextSourceLength;
+
break;
}
}
@@ -538,7 +556,9 @@ namespace Avalonia.Media.TextFormatting
if (lastDirection == currentDirection && result.Count > 0 && MathUtilities.AreClose(currentRect.Right, startX))
{
- var textBounds = new TextBounds(currentRect.WithWidth(currentRect.Width + width), currentDirection);
+ currentRect = currentRect.WithWidth(currentRect.Width + width);
+
+ var textBounds = new TextBounds(currentRect, currentDirection);
result[result.Count - 1] = textBounds;
}
@@ -551,21 +571,9 @@ namespace Avalonia.Media.TextFormatting
if (currentDirection == FlowDirection.LeftToRight)
{
- if (nextRun != null)
- {
- if (nextRun.Text.Start > currentRun.Text.Start && nextRun.Text.Start >= firstTextSourceCharacterIndex + textLength)
- {
- break;
- }
-
- currentPosition = nextRun.Text.End;
- }
- else
+ if (currentPosition >= firstTextSourceCharacterIndex + textLength)
{
- if (currentPosition >= firstTextSourceCharacterIndex + textLength)
- {
- break;
- }
+ break;
}
}
else
@@ -575,10 +583,7 @@ namespace Avalonia.Media.TextFormatting
break;
}
- if (currentPosition != currentRun.Text.Start)
- {
- endX += currentRun.Size.Width - endOffset;
- }
+ endX += currentRun.Size.Width - endOffset;
}
lastDirection = currentDirection;
@@ -1018,31 +1023,21 @@ namespace Avalonia.Media.TextFormatting
private TextLineMetrics CreateLineMetrics()
{
- var start = 0d;
- var height = 0d;
+ var glyphTypeface = _paragraphProperties.DefaultTextRunProperties.Typeface.GlyphTypeface;
+ var fontRenderingEmSize = _paragraphProperties.DefaultTextRunProperties.FontRenderingEmSize;
+ var scale = fontRenderingEmSize / glyphTypeface.DesignEmHeight;
+
var width = 0d;
var widthIncludingWhitespace = 0d;
var trailingWhitespaceLength = 0;
var newLineLength = 0;
- var ascent = 0d;
- var descent = 0d;
- var lineGap = 0d;
- var fontRenderingEmSize = 0d;
+ var ascent = glyphTypeface.Ascent * scale;
+ var descent = glyphTypeface.Descent * scale;
+ var lineGap = glyphTypeface.LineGap * scale;
- var lineHeight = _paragraphProperties.LineHeight;
-
- if (_textRuns.Count == 0)
- {
- var glyphTypeface = _paragraphProperties.DefaultTextRunProperties.Typeface.GlyphTypeface;
- fontRenderingEmSize = _paragraphProperties.DefaultTextRunProperties.FontRenderingEmSize;
- var scale = fontRenderingEmSize / glyphTypeface.DesignEmHeight;
- ascent = glyphTypeface.Ascent * scale;
- height = double.IsNaN(lineHeight) || MathUtilities.IsZero(lineHeight) ?
- descent - ascent + lineGap :
- lineHeight;
-
- return new TextLineMetrics(false, height, 0, start, -ascent, 0, 0, 0);
- }
+ var height = descent - ascent + lineGap;
+
+ var lineHeight = _paragraphProperties.LineHeight;
for (var index = 0; index < _textRuns.Count; index++)
{
@@ -1166,12 +1161,15 @@ namespace Avalonia.Media.TextFormatting
}
}
- start = GetParagraphOffsetX(width, widthIncludingWhitespace, _paragraphWidth,
+ var start = GetParagraphOffsetX(width, widthIncludingWhitespace, _paragraphWidth,
_paragraphProperties.TextAlignment, _paragraphProperties.FlowDirection);
if (!double.IsNaN(lineHeight) && !MathUtilities.IsZero(lineHeight))
{
- height = lineHeight;
+ if(lineHeight > height)
+ {
+ height = lineHeight;
+ }
}
return new TextLineMetrics(widthIncludingWhitespace > _paragraphWidth, height, newLineLength, start,
diff --git a/src/Avalonia.Base/Media/TextFormatting/Unicode/BiDiAlgorithm.cs b/src/Avalonia.Base/Media/TextFormatting/Unicode/BiDiAlgorithm.cs
index d18a4b2a87..2511807d9c 100644
--- a/src/Avalonia.Base/Media/TextFormatting/Unicode/BiDiAlgorithm.cs
+++ b/src/Avalonia.Base/Media/TextFormatting/Unicode/BiDiAlgorithm.cs
@@ -238,6 +238,11 @@ namespace Avalonia.Media.TextFormatting.Unicode
_levelRuns.Clear();
_resolvedLevelsBuffer.Clear();
+ if (types.IsEmpty)
+ {
+ return;
+ }
+
// Setup original types and working types
_originalClasses = types;
_workingClasses = _workingClassesBuffer.Add(types);
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.Base/Styling/IStyle.cs b/src/Avalonia.Base/Styling/IStyle.cs
index 78fbe0f2b5..738a69cb88 100644
--- a/src/Avalonia.Base/Styling/IStyle.cs
+++ b/src/Avalonia.Base/Styling/IStyle.cs
@@ -8,7 +8,7 @@ namespace Avalonia.Styling
///
/// Defines the interface for styles.
///
- public interface IStyle
+ public interface IStyle : IResourceNode
{
///
/// Gets a collection of child styles.
diff --git a/src/Avalonia.Base/Styling/Styles.cs b/src/Avalonia.Base/Styling/Styles.cs
index 81502f1570..d79081152e 100644
--- a/src/Avalonia.Base/Styling/Styles.cs
+++ b/src/Avalonia.Base/Styling/Styles.cs
@@ -160,7 +160,7 @@ namespace Avalonia.Styling
for (var i = Count - 1; i >= 0; --i)
{
- if (this[i] is IResourceProvider p && p.TryGetResource(key, out value))
+ if (this[i].TryGetResource(key, out value))
{
return true;
}
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/ColorComponent.cs b/src/Avalonia.Controls.ColorPicker/ColorComponent.cs
new file mode 100644
index 0000000000..71725056cf
--- /dev/null
+++ b/src/Avalonia.Controls.ColorPicker/ColorComponent.cs
@@ -0,0 +1,28 @@
+namespace Avalonia.Controls
+{
+ ///
+ /// Defines a specific component within a color model.
+ ///
+ public enum ColorComponent
+ {
+ ///
+ /// Represents the alpha component.
+ ///
+ Alpha = 0,
+
+ ///
+ /// Represents the first color component which is Red when RGB or Hue when HSV.
+ ///
+ Component1 = 1,
+
+ ///
+ /// Represents the second color component which is Green when RGB or Saturation when HSV.
+ ///
+ Component2 = 2,
+
+ ///
+ /// Represents the third color component which is Blue when RGB or Value when HSV.
+ ///
+ Component3 = 3
+ }
+}
diff --git a/src/Avalonia.Controls.ColorPicker/ColorModel.cs b/src/Avalonia.Controls.ColorPicker/ColorModel.cs
new file mode 100644
index 0000000000..f11b514706
--- /dev/null
+++ b/src/Avalonia.Controls.ColorPicker/ColorModel.cs
@@ -0,0 +1,18 @@
+namespace Avalonia.Controls
+{
+ ///
+ /// Defines the model used to represent colors.
+ ///
+ public enum ColorModel
+ {
+ ///
+ /// Color is represented by hue, saturation, value and alpha components.
+ ///
+ Hsva,
+
+ ///
+ /// Color is represented by red, green, blue and alpha components.
+ ///
+ Rgba
+ }
+}
diff --git a/src/Avalonia.Controls.ColorPicker/ColorPreviewer/ColorPreviewer.Properties.cs b/src/Avalonia.Controls.ColorPicker/ColorPreviewer/ColorPreviewer.Properties.cs
new file mode 100644
index 0000000000..0fa6ab8083
--- /dev/null
+++ b/src/Avalonia.Controls.ColorPicker/ColorPreviewer/ColorPreviewer.Properties.cs
@@ -0,0 +1,50 @@
+using Avalonia.Data;
+using Avalonia.Media;
+
+namespace Avalonia.Controls.Primitives
+{
+ ///
+ public partial class ColorPreviewer
+ {
+ ///
+ /// Defines the property.
+ ///
+ public static readonly StyledProperty HsvColorProperty =
+ AvaloniaProperty.Register(
+ nameof(HsvColor),
+ Colors.Transparent.ToHsv(),
+ defaultBindingMode: BindingMode.TwoWay);
+
+ ///
+ /// Defines the property.
+ ///
+ public static readonly StyledProperty ShowAccentColorsProperty =
+ AvaloniaProperty.Register(
+ nameof(ShowAccentColors),
+ true);
+
+ ///
+ /// Gets or sets the currently previewed color in the HSV color model.
+ ///
+ ///
+ /// Only an HSV color is supported in this control to ensure there is never any
+ /// loss of precision or color information. Accent colors, like the color spectrum,
+ /// only operate with the HSV color model.
+ ///
+ public HsvColor HsvColor
+ {
+ get => GetValue(HsvColorProperty);
+ set => SetValue(HsvColorProperty, value);
+ }
+
+ ///
+ /// Gets or sets a value indicating whether accent colors are shown along
+ /// with the preview color.
+ ///
+ public bool ShowAccentColors
+ {
+ get => GetValue(ShowAccentColorsProperty);
+ set => SetValue(ShowAccentColorsProperty, value);
+ }
+ }
+}
diff --git a/src/Avalonia.Controls.ColorPicker/ColorPreviewer/ColorPreviewer.cs b/src/Avalonia.Controls.ColorPicker/ColorPreviewer/ColorPreviewer.cs
new file mode 100644
index 0000000000..d04ddf4bd6
--- /dev/null
+++ b/src/Avalonia.Controls.ColorPicker/ColorPreviewer/ColorPreviewer.cs
@@ -0,0 +1,130 @@
+using System;
+using System.Globalization;
+using Avalonia.Controls.Metadata;
+using Avalonia.Controls.Primitives.Converters;
+using Avalonia.Input;
+using Avalonia.Media;
+
+namespace Avalonia.Controls.Primitives
+{
+ ///
+ /// Presents a preview color with optional accent colors.
+ ///
+ [TemplatePart(Name = nameof(AccentDec1Border), Type = typeof(Border))]
+ [TemplatePart(Name = nameof(AccentDec2Border), Type = typeof(Border))]
+ [TemplatePart(Name = nameof(AccentInc1Border), Type = typeof(Border))]
+ [TemplatePart(Name = nameof(AccentInc2Border), Type = typeof(Border))]
+ public partial class ColorPreviewer : TemplatedControl
+ {
+ ///
+ /// Event for when the selected color changes within the previewer.
+ /// This occurs when an accent color is pressed.
+ ///
+ public event EventHandler? ColorChanged;
+
+ private bool eventsConnected = false;
+
+ private Border? AccentDec1Border;
+ private Border? AccentDec2Border;
+ private Border? AccentInc1Border;
+ private Border? AccentInc2Border;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public ColorPreviewer() : base()
+ {
+ }
+
+ ///
+ /// Connects or disconnects all control event handlers.
+ ///
+ /// True to connect event handlers, otherwise false.
+ private void ConnectEvents(bool connected)
+ {
+ if (connected == true && eventsConnected == false)
+ {
+ // Add all events
+ if (AccentDec1Border != null) { AccentDec1Border.PointerPressed += AccentBorder_PointerPressed; }
+ if (AccentDec2Border != null) { AccentDec2Border.PointerPressed += AccentBorder_PointerPressed; }
+ if (AccentInc1Border != null) { AccentInc1Border.PointerPressed += AccentBorder_PointerPressed; }
+ if (AccentInc2Border != null) { AccentInc2Border.PointerPressed += AccentBorder_PointerPressed; }
+
+ eventsConnected = true;
+ }
+ else if (connected == false && eventsConnected == true)
+ {
+ // Remove all events
+ if (AccentDec1Border != null) { AccentDec1Border.PointerPressed -= AccentBorder_PointerPressed; }
+ if (AccentDec2Border != null) { AccentDec2Border.PointerPressed -= AccentBorder_PointerPressed; }
+ if (AccentInc1Border != null) { AccentInc1Border.PointerPressed -= AccentBorder_PointerPressed; }
+ if (AccentInc2Border != null) { AccentInc2Border.PointerPressed -= AccentBorder_PointerPressed; }
+
+ eventsConnected = false;
+ }
+ }
+
+ ///
+ protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
+ {
+ // Remove any existing events present if the control was previously loaded then unloaded
+ ConnectEvents(false);
+
+ AccentDec1Border = e.NameScope.Find(nameof(AccentDec1Border));
+ AccentDec2Border = e.NameScope.Find(nameof(AccentDec2Border));
+ AccentInc1Border = e.NameScope.Find(nameof(AccentInc1Border));
+ AccentInc2Border = e.NameScope.Find(nameof(AccentInc2Border));
+
+ // Must connect after controls are found
+ ConnectEvents(true);
+
+ base.OnApplyTemplate(e);
+ }
+
+ ///
+ protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
+ {
+ if (change.Property == HsvColorProperty)
+ {
+ OnColorChanged(new ColorChangedEventArgs(
+ change.GetOldValue().ToRgb(),
+ change.GetNewValue().ToRgb()));
+ }
+
+ base.OnPropertyChanged(change);
+ }
+
+ ///
+ /// Called before the event occurs.
+ ///
+ /// The defining old/new colors.
+ protected virtual void OnColorChanged(ColorChangedEventArgs e)
+ {
+ ColorChanged?.Invoke(this, e);
+ }
+
+ ///
+ /// Event handler for when an accent color border is pressed.
+ /// This will update the color to the background of the pressed panel.
+ ///
+ private void AccentBorder_PointerPressed(object? sender, PointerPressedEventArgs e)
+ {
+ Border? border = sender as Border;
+ int accentStep = 0;
+ HsvColor hsvColor = HsvColor;
+
+ // Get the value component delta
+ try
+ {
+ accentStep = int.Parse(border?.Tag?.ToString() ?? "", CultureInfo.InvariantCulture);
+ }
+ catch { }
+
+ HsvColor newHsvColor = AccentColorConverter.GetAccent(hsvColor, accentStep);
+ HsvColor oldHsvColor = HsvColor;
+
+ HsvColor = newHsvColor;
+ OnColorChanged(new ColorChangedEventArgs(oldHsvColor.ToRgb(), newHsvColor.ToRgb()));
+ }
+ }
+}
diff --git a/src/Avalonia.Controls.ColorPicker/ColorSlider/ColorSlider.Properties.cs b/src/Avalonia.Controls.ColorPicker/ColorSlider/ColorSlider.Properties.cs
new file mode 100644
index 0000000000..31bd296288
--- /dev/null
+++ b/src/Avalonia.Controls.ColorPicker/ColorSlider/ColorSlider.Properties.cs
@@ -0,0 +1,146 @@
+using Avalonia.Data;
+using Avalonia.Media;
+
+namespace Avalonia.Controls.Primitives
+{
+ ///
+ public partial class ColorSlider
+ {
+ ///
+ /// Defines the property.
+ ///
+ public static readonly StyledProperty ColorProperty =
+ AvaloniaProperty.Register(
+ nameof(Color),
+ Colors.White,
+ defaultBindingMode: BindingMode.TwoWay);
+
+ ///
+ /// Defines the property.
+ ///
+ public static readonly StyledProperty ColorComponentProperty =
+ AvaloniaProperty.Register(
+ nameof(ColorComponent),
+ ColorComponent.Component1);
+
+ ///
+ /// Defines the property.
+ ///
+ public static readonly StyledProperty ColorModelProperty =
+ AvaloniaProperty.Register(
+ nameof(ColorModel),
+ ColorModel.Rgba);
+
+ ///
+ /// Defines the property.
+ ///
+ public static readonly StyledProperty HsvColorProperty =
+ AvaloniaProperty.Register(
+ nameof(HsvColor),
+ Colors.White.ToHsv(),
+ defaultBindingMode: BindingMode.TwoWay);
+
+ ///
+ /// Defines the property.
+ ///
+ public static readonly StyledProperty IsAlphaMaxForcedProperty =
+ AvaloniaProperty.Register(
+ nameof(IsAlphaMaxForced),
+ true);
+
+ ///
+ /// Defines the property.
+ ///
+ public static readonly StyledProperty IsAutoUpdatingEnabledProperty =
+ AvaloniaProperty.Register(
+ nameof(IsAutoUpdatingEnabled),
+ true);
+
+ ///
+ /// Defines the property.
+ ///
+ public static readonly StyledProperty IsSaturationValueMaxForcedProperty =
+ AvaloniaProperty.Register(
+ nameof(IsSaturationValueMaxForced),
+ true);
+
+ ///
+ /// Gets or sets the currently selected color in the RGB color model.
+ ///
+ ///
+ /// Use this property instead of when in
+ /// to avoid loss of precision and color drifting.
+ ///
+ public Color Color
+ {
+ get => GetValue(ColorProperty);
+ set => SetValue(ColorProperty, value);
+ }
+
+ ///
+ /// Gets or sets the color component represented by the slider.
+ ///
+ public ColorComponent ColorComponent
+ {
+ get => GetValue(ColorComponentProperty);
+ set => SetValue(ColorComponentProperty, value);
+ }
+
+ ///
+ /// Gets or sets the active color model used by the slider.
+ ///
+ public ColorModel ColorModel
+ {
+ get => GetValue(ColorModelProperty);
+ set => SetValue(ColorModelProperty, value);
+ }
+
+ ///
+ /// Gets or sets the currently selected color in the HSV color model.
+ ///
+ ///
+ /// Use this property instead of when in
+ /// to avoid loss of precision and color drifting.
+ ///
+ public HsvColor HsvColor
+ {
+ get => GetValue(HsvColorProperty);
+ set => SetValue(HsvColorProperty, value);
+ }
+
+ ///
+ /// Gets or sets a value indicating whether the alpha component is always forced to maximum for components
+ /// other than .
+ /// This ensures that the background is always visible and never transparent regardless of the actual color.
+ ///
+ public bool IsAlphaMaxForced
+ {
+ get => GetValue(IsAlphaMaxForcedProperty);
+ set => SetValue(IsAlphaMaxForcedProperty, value);
+ }
+
+ ///
+ /// Gets or sets a value indicating whether automatic background and foreground updates will be
+ /// calculated when the set color changes.
+ ///
+ ///
+ /// This can be disabled for performance reasons when working with multiple sliders.
+ ///
+ public bool IsAutoUpdatingEnabled
+ {
+ get => GetValue(IsAutoUpdatingEnabledProperty);
+ set => SetValue(IsAutoUpdatingEnabledProperty, value);
+ }
+
+ ///
+ /// Gets or sets a value indicating whether the saturation and value components are always forced to maximum values
+ /// when using the HSVA color model. Only component values other than will be changed.
+ /// This ensures, for example, that the Hue background is always visible and never washed out regardless of the actual color.
+ ///
+ public bool IsSaturationValueMaxForced
+ {
+ get => GetValue(IsSaturationValueMaxForcedProperty);
+ set => SetValue(IsSaturationValueMaxForcedProperty, value);
+ }
+ }
+}
diff --git a/src/Avalonia.Controls.ColorPicker/ColorSlider/ColorSlider.cs b/src/Avalonia.Controls.ColorPicker/ColorSlider/ColorSlider.cs
new file mode 100644
index 0000000000..3c38c6ed1b
--- /dev/null
+++ b/src/Avalonia.Controls.ColorPicker/ColorSlider/ColorSlider.cs
@@ -0,0 +1,399 @@
+using System;
+using Avalonia.Controls.Metadata;
+using Avalonia.Layout;
+using Avalonia.Media;
+using Avalonia.Utilities;
+
+namespace Avalonia.Controls.Primitives
+{
+ ///
+ /// A slider with a background that represents a single color component.
+ ///
+ [PseudoClasses(pcDarkSelector, pcLightSelector)]
+ public partial class ColorSlider : Slider
+ {
+ protected const string pcDarkSelector = ":dark-selector";
+ protected const string pcLightSelector = ":light-selector";
+
+ ///
+ /// Event for when the selected color changes within the slider.
+ ///
+ public event EventHandler? ColorChanged;
+
+ private const double MaxHue = 359.99999999999999999; // 17 decimal places
+ private bool disableUpdates = false;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public ColorSlider() : base()
+ {
+ }
+
+ ///
+ /// Updates the visual state of the control by applying latest PseudoClasses.
+ ///
+ private void UpdatePseudoClasses()
+ {
+ // The slider itself can be transparent for certain color values.
+ // This causes an issue where a white selector thumb over a light window background or
+ // a black selector thumb over a dark window background is not visible.
+ // This means under a certain alpha threshold, neither a white or black selector thumb
+ // should be shown and instead the default slider thumb color should be used instead.
+ if (Color.A < 128 &&
+ (IsAlphaMaxForced == false ||
+ ColorComponent == ColorComponent.Alpha))
+ {
+ PseudoClasses.Set(pcDarkSelector, false);
+ PseudoClasses.Set(pcLightSelector, false);
+ }
+ else
+ {
+ Color perceivedColor;
+
+ if (ColorModel == ColorModel.Hsva)
+ {
+ perceivedColor = GetEquivalentBackgroundColor(HsvColor).ToRgb();
+ }
+ else
+ {
+ perceivedColor = GetEquivalentBackgroundColor(Color);
+ }
+
+ if (ColorHelper.GetRelativeLuminance(perceivedColor) <= 0.5)
+ {
+ PseudoClasses.Set(pcDarkSelector, false);
+ PseudoClasses.Set(pcLightSelector, true);
+ }
+ else
+ {
+ PseudoClasses.Set(pcDarkSelector, true);
+ PseudoClasses.Set(pcLightSelector, false);
+ }
+ }
+ }
+
+ ///
+ /// Generates a new background image for the color slider and applies it.
+ ///
+ private async void UpdateBackground()
+ {
+ // 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.
+
+ var scale = LayoutHelper.GetLayoutScale(this);
+ var pixelWidth = Convert.ToInt32(Bounds.Width * scale);
+ var pixelHeight = Convert.ToInt32(Bounds.Height * scale);
+
+ if (pixelWidth != 0 && pixelHeight != 0)
+ {
+ var bitmap = await ColorPickerHelpers.CreateComponentBitmapAsync(
+ pixelWidth,
+ pixelHeight,
+ Orientation,
+ ColorModel,
+ ColorComponent,
+ HsvColor,
+ IsAlphaMaxForced,
+ IsSaturationValueMaxForced);
+
+ if (bitmap != null)
+ {
+ Background = new ImageBrush(ColorPickerHelpers.CreateBitmapFromPixelData(bitmap, pixelWidth, pixelHeight));
+ }
+ }
+ }
+
+ ///
+ /// 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)
+ {
+ // Note: Components converted into a usable range for the user
+ switch (component)
+ {
+ case ColorComponent.Alpha:
+ Minimum = 0;
+ Maximum = 100;
+ Value = hsvColor.A * 100;
+ break;
+ case ColorComponent.Component1: // Hue
+ Minimum = 0;
+ Maximum = MaxHue;
+ 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.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;
+ }
+ }
+ }
+
+ ///
+ /// 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);
+
+ var baseHsvColor = HsvColor;
+ var baseRgbColor = Color;
+ var component = ColorComponent;
+
+ if (ColorModel == ColorModel.Hsva)
+ {
+ switch (component)
+ {
+ case ColorComponent.Alpha:
+ {
+ hsvColor = new HsvColor(sliderPercent, baseHsvColor.H, baseHsvColor.S, baseHsvColor.V);
+ break;
+ }
+ case ColorComponent.Component1:
+ {
+ hsvColor = new HsvColor(baseHsvColor.A, sliderPercent * MaxHue, baseHsvColor.S, baseHsvColor.V);
+ break;
+ }
+ case ColorComponent.Component2:
+ {
+ hsvColor = new HsvColor(baseHsvColor.A, baseHsvColor.H, sliderPercent, baseHsvColor.V);
+ break;
+ }
+ case ColorComponent.Component3:
+ {
+ hsvColor = new HsvColor(baseHsvColor.A, baseHsvColor.H, baseHsvColor.S, sliderPercent);
+ break;
+ }
+ }
+
+ return (hsvColor.ToRgb(), hsvColor);
+ }
+ else
+ {
+ 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(baseRgbColor.A, componentValue, baseRgbColor.G, baseRgbColor.B);
+ break;
+ case ColorComponent.Component2:
+ rgbColor = new Color(baseRgbColor.A, baseRgbColor.R, componentValue, baseRgbColor.B);
+ break;
+ case ColorComponent.Component3:
+ rgbColor = new Color(baseRgbColor.A, baseRgbColor.R, baseRgbColor.G, componentValue);
+ break;
+ }
+
+ return (rgbColor, rgbColor.ToHsv());
+ }
+ }
+
+ ///
+ /// 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.
+ ///
+ /// The actual color to get the equivalent background color for.
+ /// The equivalent, perceived background color.
+ private HsvColor GetEquivalentBackgroundColor(HsvColor hsvColor)
+ {
+ var component = ColorComponent;
+ var isAlphaMaxForced = IsAlphaMaxForced;
+ var isSaturationValueMaxForced = IsSaturationValueMaxForced;
+
+ if (isAlphaMaxForced &&
+ component != ColorComponent.Alpha)
+ {
+ hsvColor = new HsvColor(1.0, hsvColor.H, hsvColor.S, hsvColor.V);
+ }
+
+ switch (component)
+ {
+ 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;
+
+ if (isAlphaMaxForced &&
+ component != ColorComponent.Alpha)
+ {
+ rgbColor = new Color(255, rgbColor.R, rgbColor.G, rgbColor.B);
+ }
+
+ return rgbColor;
+ }
+
+ ///
+ protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
+ {
+ if (disableUpdates)
+ {
+ base.OnPropertyChanged(change);
+ return;
+ }
+
+ // Always keep the two color properties in sync
+ if (change.Property == ColorProperty)
+ {
+ disableUpdates = true;
+
+ HsvColor = Color.ToHsv();
+
+ if (IsAutoUpdatingEnabled)
+ {
+ SetColorToSliderValues();
+ UpdateBackground();
+ }
+
+ UpdatePseudoClasses();
+ OnColorChanged(new ColorChangedEventArgs(
+ change.GetOldValue(),
+ change.GetNewValue()));
+
+ disableUpdates = false;
+ }
+ else if (change.Property == HsvColorProperty)
+ {
+ disableUpdates = true;
+
+ Color = HsvColor.ToRgb();
+
+ if (IsAutoUpdatingEnabled)
+ {
+ SetColorToSliderValues();
+ UpdateBackground();
+ }
+
+ UpdatePseudoClasses();
+ OnColorChanged(new ColorChangedEventArgs(
+ change.GetOldValue().ToRgb(),
+ change.GetNewValue().ToRgb()));
+
+ disableUpdates = false;
+ }
+ else if (change.Property == BoundsProperty)
+ {
+ if (IsAutoUpdatingEnabled)
+ {
+ UpdateBackground();
+ }
+ }
+ else if (change.Property == ValueProperty ||
+ change.Property == MinimumProperty ||
+ change.Property == MaximumProperty)
+ {
+ disableUpdates = true;
+
+ Color oldColor = Color;
+ (var color, var hsvColor) = GetColorFromSliderValues();
+
+ if (ColorModel == ColorModel.Hsva)
+ {
+ HsvColor = hsvColor;
+ Color = hsvColor.ToRgb();
+ }
+ else
+ {
+ Color = color;
+ HsvColor = color.ToHsv();
+ }
+
+ UpdatePseudoClasses();
+ OnColorChanged(new ColorChangedEventArgs(oldColor, Color));
+
+ disableUpdates = false;
+ }
+
+ base.OnPropertyChanged(change);
+ }
+
+ ///
+ /// Called before the event occurs.
+ ///
+ /// The defining old/new colors.
+ protected virtual void OnColorChanged(ColorChangedEventArgs e)
+ {
+ ColorChanged?.Invoke(this, e);
+ }
+ }
+}
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..587a89ee38
--- /dev/null
+++ b/src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.Properties.cs
@@ -0,0 +1,222 @@
+// 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.Data;
+using Avalonia.Media;
+
+namespace Avalonia.Controls.Primitives
+{
+ ///
+ public partial class ColorSpectrum
+ {
+ ///
+ /// Defines the property.
+ ///
+ public static readonly StyledProperty ColorProperty =
+ AvaloniaProperty.Register(
+ nameof(Color),
+ Colors.White,
+ defaultBindingMode: BindingMode.TwoWay);
+
+ ///
+ /// Defines the property.
+ ///
+ public static readonly StyledProperty ComponentsProperty =
+ AvaloniaProperty.Register(
+ nameof(Components),
+ ColorSpectrumComponents.HueSaturation);
+
+ ///
+ /// Defines the property.
+ ///
+ public static readonly StyledProperty HsvColorProperty =
+ AvaloniaProperty.Register(
+ nameof(HsvColor),
+ Colors.White.ToHsv(),
+ defaultBindingMode: BindingMode.TwoWay);
+
+ ///
+ /// Defines the property.
+ ///
+ public static readonly StyledProperty MaxHueProperty =
+ AvaloniaProperty.Register(
+ nameof(MaxHue),
+ 359);
+
+ ///
+ /// Defines the property.
+ ///
+ public static readonly StyledProperty MaxSaturationProperty =
+ AvaloniaProperty.Register(
+ nameof(MaxSaturation),
+ 100);
+
+ ///
+ /// Defines the property.
+ ///
+ public static readonly StyledProperty MaxValueProperty =
+ AvaloniaProperty.Register(
+ nameof(MaxValue),
+ 100);
+
+ ///
+ /// Defines the property.
+ ///
+ public static readonly StyledProperty MinHueProperty =
+ AvaloniaProperty.Register(
+ nameof(MinHue),
+ 0);
+
+ ///
+ /// Defines the property.
+ ///
+ public static readonly StyledProperty MinSaturationProperty =
+ AvaloniaProperty.Register(
+ nameof(MinSaturation),
+ 0);
+
+ ///
+ /// Defines the property.
+ ///
+ public static readonly StyledProperty MinValueProperty =
+ AvaloniaProperty.Register(
+ nameof(MinValue),
+ 0);
+
+ ///
+ /// Defines the property.
+ ///
+ public static readonly StyledProperty ShapeProperty =
+ AvaloniaProperty.Register(
+ nameof(Shape),
+ ColorSpectrumShape.Box);
+
+ ///
+ /// 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);
+ }
+
+ ///
+ /// 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);
+ }
+
+ ///
+ /// 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);
+ }
+
+ ///
+ /// 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);
+ }
+
+ ///
+ /// 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);
+ }
+
+ ///
+ /// 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);
+ }
+
+ ///
+ /// 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);
+ }
+
+ ///
+ /// 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);
+ }
+
+ ///
+ /// 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);
+ }
+
+ ///
+ /// Gets or sets the displayed shape of the spectrum.
+ ///
+ public ColorSpectrumShape Shape
+ {
+ get => GetValue(ShapeProperty);
+ set => SetValue(ShapeProperty, value);
+ }
+ }
+}
diff --git a/src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.cs b/src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.cs
new file mode 100644
index 0000000000..245592207e
--- /dev/null
+++ b/src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.cs
@@ -0,0 +1,1606 @@
+// 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.Layout;
+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_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, pcDarkSelector, pcLightSelector)]
+ public partial class ColorSpectrum : TemplatedControl
+ {
+ protected const string pcPressed = ":pressed";
+ protected const string pcDarkSelector = ":dark-selector";
+ 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;
+
+ // 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
+
+ _inputTarget = e.NameScope.Find
+
diff --git a/src/Avalonia.Themes.Default/Controls/DataValidationErrors.xaml b/src/Avalonia.Themes.Default/Controls/DataValidationErrors.xaml
index a3a4cf4662..d7bf4bbbf1 100644
--- a/src/Avalonia.Themes.Default/Controls/DataValidationErrors.xaml
+++ b/src/Avalonia.Themes.Default/Controls/DataValidationErrors.xaml
@@ -1,9 +1,13 @@
-
+
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/Markup/Avalonia.Markup.Xaml/MarkupExtensions/StaticResourceExtension.cs b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/StaticResourceExtension.cs
index db33b88cc3..add97a660b 100644
--- a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/StaticResourceExtension.cs
+++ b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/StaticResourceExtension.cs
@@ -39,17 +39,11 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions
targetType = setter.Property.PropertyType;
}
- // Look upwards though the ambient context for IResourceHosts and IResourceProviders
+ // Look upwards though the ambient context for IResourceNodes
// which might be able to give us the resource.
- foreach (var e in stack.Parents)
+ foreach (var parent in stack.Parents)
{
- object value;
-
- if (e is IResourceHost host && host.TryGetResource(ResourceKey, out value))
- {
- return ColorToBrushConverter.Convert(value, targetType);
- }
- else if (e is IResourceProvider provider && provider.TryGetResource(ResourceKey, out value))
+ if (parent is IResourceNode node && node.TryGetResource(ResourceKey, out var value))
{
return ColorToBrushConverter.Convert(value, targetType);
}
@@ -58,7 +52,11 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions
if (provideTarget.TargetObject is IControl target &&
provideTarget.TargetProperty is PropertyInfo property)
{
- DelayedBinding.Add(target, property, x => GetValue(x, targetType));
+ // This is stored locally to avoid allocating closure in the outer scope.
+ var localTargetType = targetType;
+ var localInstance = this;
+
+ DelayedBinding.Add(target, property, x => localInstance.GetValue(x, localTargetType));
return AvaloniaProperty.UnsetValue;
}
diff --git a/src/Markup/Avalonia.Markup.Xaml/Styling/StyleInclude.cs b/src/Markup/Avalonia.Markup.Xaml/Styling/StyleInclude.cs
index 607b552c28..fa4a27fc50 100644
--- a/src/Markup/Avalonia.Markup.Xaml/Styling/StyleInclude.cs
+++ b/src/Markup/Avalonia.Markup.Xaml/Styling/StyleInclude.cs
@@ -60,7 +60,7 @@ namespace Avalonia.Markup.Xaml.Styling
}
}
- bool IResourceNode.HasResources => (Loaded as IResourceProvider)?.HasResources ?? false;
+ bool IResourceNode.HasResources => Loaded?.HasResources ?? false;
IReadOnlyList IStyle.Children => _loaded ?? Array.Empty();
@@ -86,9 +86,9 @@ namespace Avalonia.Markup.Xaml.Styling
public bool TryGetResource(object key, out object? value)
{
- if (!_isLoading && Loaded is IResourceProvider p)
+ if (!_isLoading)
{
- return p.TryGetResource(key, out value);
+ return Loaded.TryGetResource(key, out value);
}
value = null;
diff --git a/src/Skia/Avalonia.Skia/TextShaperImpl.cs b/src/Skia/Avalonia.Skia/TextShaperImpl.cs
index a0890262e7..ebaa247da8 100644
--- a/src/Skia/Avalonia.Skia/TextShaperImpl.cs
+++ b/src/Skia/Avalonia.Skia/TextShaperImpl.cs
@@ -1,6 +1,5 @@
using System;
using System.Globalization;
-using Avalonia.Media;
using Avalonia.Media.TextFormatting;
using Avalonia.Media.TextFormatting.Unicode;
using Avalonia.Platform;
@@ -59,7 +58,7 @@ namespace Avalonia.Skia
var glyphIndex = (ushort)sourceInfo.Codepoint;
- var glyphCluster = (int)sourceInfo.Cluster;
+ var glyphCluster = (int)(sourceInfo.Cluster);
var glyphAdvance = GetGlyphAdvance(glyphPositions, i, textScale);
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 @@
+
diff --git a/tests/Avalonia.RenderTests/Media/TextFormatting/TextLayoutTests.cs b/tests/Avalonia.RenderTests/Media/TextFormatting/TextLayoutTests.cs
index 6ed4ba0d4a..b668f4d39e 100644
--- a/tests/Avalonia.RenderTests/Media/TextFormatting/TextLayoutTests.cs
+++ b/tests/Avalonia.RenderTests/Media/TextFormatting/TextLayoutTests.cs
@@ -1,11 +1,8 @@
using Avalonia.Media;
-using Avalonia.Platform;
using System;
-using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Runtime.InteropServices;
-using System.Text;
using System.Threading.Tasks;
using Avalonia.Controls;
using Avalonia.Media.TextFormatting;
diff --git a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs
index e9bc792be3..a47638d2ec 100644
--- a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs
+++ b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs
@@ -70,12 +70,12 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
}
}
}
-
+
[Fact]
public void Should_Get_Next_Caret_CharacterHit_Bidi()
{
const string text = "אבג 1 ABC";
-
+
using (Start())
{
var defaultProperties = new GenericTextRunProperties(Typeface.Default);
@@ -90,7 +90,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
var clusters = new List();
- foreach (var textRun in textLine.TextRuns.OrderBy(x=> x.Text.Start))
+ foreach (var textRun in textLine.TextRuns.OrderBy(x => x.Text.Start))
{
var shapedRun = (ShapedTextCharacters)textRun;
@@ -98,7 +98,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
shapedRun.ShapedBuffer.GlyphClusters.Reverse() :
shapedRun.ShapedBuffer.GlyphClusters);
}
-
+
var nextCharacterHit = new CharacterHit(0, clusters[1] - clusters[0]);
foreach (var cluster in clusters)
@@ -122,7 +122,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
public void Should_Get_Previous_Caret_CharacterHit_Bidi()
{
const string text = "אבג 1 ABC";
-
+
using (Start())
{
var defaultProperties = new GenericTextRunProperties(Typeface.Default);
@@ -137,7 +137,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
var clusters = new List();
- foreach (var textRun in textLine.TextRuns.OrderBy(x=> x.Text.Start))
+ foreach (var textRun in textLine.TextRuns.OrderBy(x => x.Text.Start))
{
var shapedRun = (ShapedTextCharacters)textRun;
@@ -147,13 +147,13 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
}
clusters.Reverse();
-
+
var nextCharacterHit = new CharacterHit(text.Length - 1);
foreach (var cluster in clusters)
{
var currentCaretIndex = nextCharacterHit.FirstCharacterIndex + nextCharacterHit.TrailingLength;
-
+
Assert.Equal(cluster, currentCaretIndex);
nextCharacterHit = textLine.GetPreviousCaretCharacterHit(nextCharacterHit);
@@ -168,7 +168,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
Assert.Equal(lastCharacterHit.TrailingLength, nextCharacterHit.TrailingLength);
}
}
-
+
[InlineData("𐐷𐐷𐐷𐐷𐐷")]
[InlineData("01234567🎉\n")]
[InlineData("𐐷1234")]
@@ -324,7 +324,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
}
}
- Assert.Equal(currentDistance,textLine.GetDistanceFromCharacterHit(new CharacterHit(s_multiLineText.Length)));
+ Assert.Equal(currentDistance, textLine.GetDistanceFromCharacterHit(new CharacterHit(s_multiLineText.Length)));
}
}
@@ -371,7 +371,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
yield return CreateData("01234 01234", 58, TextTrimming.WordEllipsis, "01234\u2026");
yield return CreateData("01234", 9, TextTrimming.CharacterEllipsis, "\u2026");
yield return CreateData("01234", 2, TextTrimming.CharacterEllipsis, "");
-
+
object[] CreateData(string text, double width, TextTrimming mode, string expected)
{
return new object[]
@@ -424,7 +424,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
{
var defaultProperties = new GenericTextRunProperties(Typeface.Default);
var textSource = new DrawableRunTextSource();
-
+
var formatter = new TextFormatterImpl();
var textLine =
@@ -471,7 +471,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
Assert.Equal(4, textLine.TextRuns.Count);
- var currentHit = textLine.GetPreviousCaretCharacterHit(new CharacterHit(3,1));
+ var currentHit = textLine.GetPreviousCaretCharacterHit(new CharacterHit(3, 1));
Assert.Equal(3, currentHit.FirstCharacterIndex);
Assert.Equal(0, currentHit.TrailingLength);
@@ -552,11 +552,11 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
switch (textSourceIndex)
{
case 0:
- return new CustomDrawableRun();
+ return new CustomDrawableRun();
case 1:
return new TextCharacters(new ReadOnlySlice(Text.AsMemory(), 1, 1, 1), new GenericTextRunProperties(Typeface.Default));
case 2:
- return new CustomDrawableRun();
+ return new CustomDrawableRun();
case 3:
return new TextCharacters(new ReadOnlySlice(Text.AsMemory(), 3, 1, 3), new GenericTextRunProperties(Typeface.Default));
default:
@@ -564,14 +564,14 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
}
}
}
-
+
private class CustomDrawableRun : DrawableTextRun
{
public override Size Size => new(14, 14);
public override double Baseline => 14;
public override void Draw(DrawingContext drawingContext, Point origin)
{
-
+
}
}
@@ -587,29 +587,29 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
var shapedTextRuns = textLine.TextRuns.Cast().ToList();
var lastCluster = -1;
-
+
foreach (var textRun in shapedTextRuns)
{
var shapedBuffer = textRun.ShapedBuffer;
var currentClusters = shapedBuffer.GlyphClusters.ToList();
- foreach (var currentCluster in currentClusters)
+ foreach (var currentCluster in currentClusters)
{
if (lastCluster == currentCluster)
{
continue;
}
-
+
glyphClusters.Add(currentCluster);
lastCluster = currentCluster;
}
}
-
+
return glyphClusters;
}
-
+
private static List BuildRects(TextLine textLine)
{
var rects = new List();
@@ -624,11 +624,11 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
foreach (var textRun in shapedTextRuns)
{
var shapedBuffer = textRun.ShapedBuffer;
-
+
for (var index = 0; index < shapedBuffer.GlyphAdvances.Count; index++)
{
var currentCluster = shapedBuffer.GlyphClusters[index];
-
+
var advance = shapedBuffer.GlyphAdvances[index];
if (lastCluster != currentCluster)
@@ -642,10 +642,10 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
rects.Remove(rect);
rect = rect.WithWidth(rect.Width + advance);
-
+
rects.Add(rect);
}
-
+
currentX += advance;
lastCluster = currentCluster;
@@ -655,8 +655,65 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
return rects;
}
+
+ [Fact]
+ public void Should_Get_TextBounds_Mixed()
+ {
+ using (Start())
+ {
+ var defaultProperties = new GenericTextRunProperties(Typeface.Default);
+ var text = "0123".AsMemory();
+ var shaperOption = new TextShaperOptions(Typeface.Default.GlyphTypeface, 10, 0, CultureInfo.CurrentCulture);
+
+ var firstRun = new ShapedTextCharacters(TextShaper.Current.ShapeText(new ReadOnlySlice(text, 1, text.Length), shaperOption), defaultProperties);
+
+ var textRuns = new List
+ {
+ new CustomDrawableRun(),
+ firstRun,
+ new CustomDrawableRun(),
+ new ShapedTextCharacters(TextShaper.Current.ShapeText(new ReadOnlySlice(text, text.Length + 2, text.Length), shaperOption), defaultProperties),
+ new CustomDrawableRun(),
+ new ShapedTextCharacters(TextShaper.Current.ShapeText(new ReadOnlySlice(text, text.Length * 2 + 3, text.Length), shaperOption), defaultProperties)
+ };
+
+ var textSource = new FixedRunsTextSource(textRuns);
+
+ var formatter = new TextFormatterImpl();
+
+ var textLine =
+ formatter.FormatLine(textSource, 0, double.PositiveInfinity,
+ new GenericTextParagraphProperties(defaultProperties));
+
+ var textBounds = textLine.GetTextBounds(0, text.Length * 3 + 3);
+
+ Assert.Equal(1, textBounds.Count);
+ Assert.Equal(textLine.WidthIncludingTrailingWhitespace, textBounds.Sum(x => x.Rectangle.Width));
+
+ textBounds = textLine.GetTextBounds(0, 1);
+
+ Assert.Equal(1, textBounds.Count);
+ Assert.Equal(14, textBounds[0].Rectangle.Width);
+
+ textBounds = textLine.GetTextBounds(0, firstRun.Text.Length + 1);
+
+ Assert.Equal(1, textBounds.Count);
+ Assert.Equal(firstRun.Size.Width + 14, textBounds[0].Rectangle.Width);
+
+ textBounds = textLine.GetTextBounds(1, firstRun.Text.Length);
+
+ Assert.Equal(1, textBounds.Count);
+ Assert.Equal(firstRun.Size.Width, textBounds[0].Rectangle.Width);
+
+ textBounds = textLine.GetTextBounds(1, firstRun.Text.Length + 1);
+
+ Assert.Equal(1, textBounds.Count);
+ Assert.Equal(firstRun.Size.Width + 14, textBounds[0].Rectangle.Width);
+ }
+ }
+
[Fact]
- public void Should_Get_TextBounds()
+ public void Should_Get_TextBounds_BiDi()
{
using (Start())
{
@@ -673,7 +730,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
new ShapedTextCharacters(TextShaper.Current.ShapeText(new ReadOnlySlice(text, text.Length * 3, text.Length), ltrOptions), defaultProperties)
};
-
+
var textSource = new FixedRunsTextSource(textRuns);
var formatter = new TextFormatterImpl();
@@ -700,12 +757,16 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
public TextRun? GetTextRun(int textSourceIndex)
{
+ var currentPosition = 0;
+
foreach (var textRun in _textRuns)
{
- if(textRun.Text.Start == textSourceIndex)
+ if (currentPosition == textSourceIndex)
{
return textRun;
}
+
+ currentPosition += textRun.TextSourceLength;
}
return null;