diff --git a/Avalonia.sln b/Avalonia.sln
index a792774d94..5043f474c2 100644
--- a/Avalonia.sln
+++ b/Avalonia.sln
@@ -117,6 +117,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Props", "Props", "{F3AC8BC1
build\Base.props = build\Base.props
build\Binding.props = build\Binding.props
build\CoreLibraries.props = build\CoreLibraries.props
+ build\DevAnalyzers.props = build\DevAnalyzers.props
build\EmbedXaml.props = build\EmbedXaml.props
build\HarfBuzzSharp.props = build\HarfBuzzSharp.props
build\JetBrains.Annotations.props = build\JetBrains.Annotations.props
@@ -234,7 +235,9 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.PlatformSupport",
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ControlCatalog.iOS", "samples\ControlCatalog.iOS\ControlCatalog.iOS.csproj", "{70B9F5CC-E2F9-4314-9514-EDE762ACCC4B}"
EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.PlatformSupport.UnitTests", "tests\Avalonia.PlatformSupport.UnitTests\Avalonia.PlatformSupport.UnitTests.csproj", "{CE910927-CE5A-456F-BC92-E4C757354A5C}"
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.PlatformSupport.UnitTests", "tests\Avalonia.PlatformSupport.UnitTests\Avalonia.PlatformSupport.UnitTests.csproj", "{CE910927-CE5A-456F-BC92-E4C757354A5C}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DevAnalyzers", "src\tools\DevAnalyzers\DevAnalyzers.csproj", "{2B390431-288C-435C-BB6B-A374033BD8D1}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
@@ -2238,6 +2241,30 @@ Global
{CE910927-CE5A-456F-BC92-E4C757354A5C}.Release|iPhone.Build.0 = Release|Any CPU
{CE910927-CE5A-456F-BC92-E4C757354A5C}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU
{CE910927-CE5A-456F-BC92-E4C757354A5C}.Release|iPhoneSimulator.Build.0 = Release|Any CPU
+ {2B390431-288C-435C-BB6B-A374033BD8D1}.Ad-Hoc|Any CPU.ActiveCfg = Debug|Any CPU
+ {2B390431-288C-435C-BB6B-A374033BD8D1}.Ad-Hoc|Any CPU.Build.0 = Debug|Any CPU
+ {2B390431-288C-435C-BB6B-A374033BD8D1}.Ad-Hoc|iPhone.ActiveCfg = Debug|Any CPU
+ {2B390431-288C-435C-BB6B-A374033BD8D1}.Ad-Hoc|iPhone.Build.0 = Debug|Any CPU
+ {2B390431-288C-435C-BB6B-A374033BD8D1}.Ad-Hoc|iPhoneSimulator.ActiveCfg = Debug|Any CPU
+ {2B390431-288C-435C-BB6B-A374033BD8D1}.Ad-Hoc|iPhoneSimulator.Build.0 = Debug|Any CPU
+ {2B390431-288C-435C-BB6B-A374033BD8D1}.AppStore|Any CPU.ActiveCfg = Debug|Any CPU
+ {2B390431-288C-435C-BB6B-A374033BD8D1}.AppStore|Any CPU.Build.0 = Debug|Any CPU
+ {2B390431-288C-435C-BB6B-A374033BD8D1}.AppStore|iPhone.ActiveCfg = Debug|Any CPU
+ {2B390431-288C-435C-BB6B-A374033BD8D1}.AppStore|iPhone.Build.0 = Debug|Any CPU
+ {2B390431-288C-435C-BB6B-A374033BD8D1}.AppStore|iPhoneSimulator.ActiveCfg = Debug|Any CPU
+ {2B390431-288C-435C-BB6B-A374033BD8D1}.AppStore|iPhoneSimulator.Build.0 = Debug|Any CPU
+ {2B390431-288C-435C-BB6B-A374033BD8D1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {2B390431-288C-435C-BB6B-A374033BD8D1}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {2B390431-288C-435C-BB6B-A374033BD8D1}.Debug|iPhone.ActiveCfg = Debug|Any CPU
+ {2B390431-288C-435C-BB6B-A374033BD8D1}.Debug|iPhone.Build.0 = Debug|Any CPU
+ {2B390431-288C-435C-BB6B-A374033BD8D1}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU
+ {2B390431-288C-435C-BB6B-A374033BD8D1}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU
+ {2B390431-288C-435C-BB6B-A374033BD8D1}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {2B390431-288C-435C-BB6B-A374033BD8D1}.Release|Any CPU.Build.0 = Release|Any CPU
+ {2B390431-288C-435C-BB6B-A374033BD8D1}.Release|iPhone.ActiveCfg = Release|Any CPU
+ {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
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -2303,6 +2330,7 @@ Global
{A0D0A6A4-5C72-4ADA-9B27-621C7D94F270} = {9B9E3891-2366-4253-A952-D08BCEB71098}
{70B9F5CC-E2F9-4314-9514-EDE762ACCC4B} = {9B9E3891-2366-4253-A952-D08BCEB71098}
{CE910927-CE5A-456F-BC92-E4C757354A5C} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B}
+ {2B390431-288C-435C-BB6B-A374033BD8D1} = {4ED8B739-6F4E-4CD4-B993-545E6B5CE637}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {87366D66-1391-4D90-8999-95A620AD786A}
diff --git a/NuGet.Config b/NuGet.Config
index 7a1f28bea7..7d2bd8abd2 100644
--- a/NuGet.Config
+++ b/NuGet.Config
@@ -5,5 +5,6 @@
+
diff --git a/azure-pipelines.yml b/azure-pipelines.yml
index 79456b117b..edf3c3d819 100644
--- a/azure-pipelines.yml
+++ b/azure-pipelines.yml
@@ -31,14 +31,14 @@ jobs:
vmImage: 'ubuntu-20.04'
steps:
- task: UseDotNet@2
- displayName: 'Use .NET Core SDK 3.1.414'
+ displayName: 'Use .NET Core SDK 3.1.418'
inputs:
- version: 3.1.414
+ version: 3.1.418
- task: UseDotNet@2
- displayName: 'Use .NET Core SDK 6.0.100'
+ displayName: 'Use .NET Core SDK 6.0.202'
inputs:
- version: 6.0.200
+ version: 6.0.202
- task: CmdLine@2
displayName: 'Run Build'
@@ -62,14 +62,14 @@ jobs:
vmImage: 'macOS-10.15'
steps:
- task: UseDotNet@2
- displayName: 'Use .NET Core SDK 3.1.414'
+ displayName: 'Use .NET Core SDK 3.1.418'
inputs:
- version: 3.1.414
+ version: 3.1.418
- task: UseDotNet@2
- displayName: 'Use .NET Core SDK 6.0.100'
+ displayName: 'Use .NET Core SDK 6.0.202'
inputs:
- version: 6.0.200
+ version: 6.0.202
- task: CmdLine@2
displayName: 'Install Mono 5.18'
@@ -134,20 +134,20 @@ jobs:
SolutionDir: '$(Build.SourcesDirectory)'
steps:
- task: UseDotNet@2
- displayName: 'Use .NET Core SDK 3.1.414'
+ displayName: 'Use .NET Core SDK 3.1.418'
inputs:
- version: 3.1.414
+ version: 3.1.418
- task: UseDotNet@2
- displayName: 'Use .NET Core SDK 6.0.100'
+ displayName: 'Use .NET Core SDK 6.0.202'
inputs:
- version: 6.0.200
+ version: 6.0.202
- task: CmdLine@2
displayName: 'Install Workloads'
inputs:
script: |
- dotnet workload install --no-cache --disable-parallel android ios --skip-manifest-update --source "https://api.nuget.org/v3/index.json"
+ dotnet workload install android ios
- task: CmdLine@2
displayName: 'Install Nuke'
diff --git a/build/DevAnalyzers.props b/build/DevAnalyzers.props
new file mode 100644
index 0000000000..28959dbd47
--- /dev/null
+++ b/build/DevAnalyzers.props
@@ -0,0 +1,9 @@
+
+
+
+
+
diff --git a/build/HarfBuzzSharp.props b/build/HarfBuzzSharp.props
index 6dd6cccb53..e10de93530 100644
--- a/build/HarfBuzzSharp.props
+++ b/build/HarfBuzzSharp.props
@@ -1,7 +1,7 @@
-
-
-
+
+
+
diff --git a/build/SkiaSharp.props b/build/SkiaSharp.props
index 60bebaad40..a217a8272d 100644
--- a/build/SkiaSharp.props
+++ b/build/SkiaSharp.props
@@ -1,7 +1,7 @@
-
-
-
+
+
+
diff --git a/global.json b/global.json
index 30265268dc..a6792b05c7 100644
--- a/global.json
+++ b/global.json
@@ -1,11 +1,10 @@
{
"sdk": {
- "version": "6.0.200",
+ "version": "6.0.202",
"rollForward": "latestFeature"
},
"msbuild-sdks": {
"Microsoft.Build.Traversal": "1.0.43",
- "Xamarin.Legacy.Sdk": "0.1.2-alpha6",
"MSBuild.Sdk.Extras": "3.0.22",
"AggregatePackage.NuGet.Sdk" : "0.1.12"
}
diff --git a/native/Avalonia.Native/src/OSX/window.mm b/native/Avalonia.Native/src/OSX/window.mm
index 620b750a40..d16c466fe6 100644
--- a/native/Avalonia.Native/src/OSX/window.mm
+++ b/native/Avalonia.Native/src/OSX/window.mm
@@ -2461,6 +2461,16 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent
if(_parent != nullptr)
{
+ auto cparent = dynamic_cast(_parent.getRaw());
+
+ if(cparent != nullptr)
+ {
+ if(cparent->WindowState() == Maximized)
+ {
+ cparent->SetWindowState(Normal);
+ }
+ }
+
_parent->GetPosition(&position);
_parent->BaseEvents->PositionChanged(position);
}
diff --git a/samples/ControlCatalog.Web/ControlCatalog.Web.csproj b/samples/ControlCatalog.Web/ControlCatalog.Web.csproj
index 520bbdf32b..b2c9ec72eb 100644
--- a/samples/ControlCatalog.Web/ControlCatalog.Web.csproj
+++ b/samples/ControlCatalog.Web/ControlCatalog.Web.csproj
@@ -1,15 +1,14 @@
net6.0
- falseenable
- True
+
+ true
+ 16777216
+ false
+ false
-
-
-
-
false
@@ -23,19 +22,36 @@
-O3-O3false
+ false
+ false
+ false
+ false
+ false
+ true
+ false
+ true
+ true
+ true
+ link
+ true
-
-
+
+
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
diff --git a/samples/ControlCatalog.Web/LinkerConfig.xml b/samples/ControlCatalog.Web/LinkerConfig.xml
deleted file mode 100644
index 5839a0fe03..0000000000
--- a/samples/ControlCatalog.Web/LinkerConfig.xml
+++ /dev/null
@@ -1,28 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/samples/ControlCatalog/MainView.xaml b/samples/ControlCatalog/MainView.xaml
index f3b52428ca..85f278b5fa 100644
--- a/samples/ControlCatalog/MainView.xaml
+++ b/samples/ControlCatalog/MainView.xaml
@@ -190,6 +190,14 @@
Mica
+
+
+ LeftToRight
+ RightToLeft
+
+
diff --git a/samples/ControlCatalog/MainView.xaml.cs b/samples/ControlCatalog/MainView.xaml.cs
index 79cf07c8d9..e8ea39abbb 100644
--- a/samples/ControlCatalog/MainView.xaml.cs
+++ b/samples/ControlCatalog/MainView.xaml.cs
@@ -76,6 +76,15 @@ namespace ControlCatalog
}
};
+ var flowDirections = this.Find("FlowDirection");
+ flowDirections.SelectionChanged += (sender, e) =>
+ {
+ if (flowDirections.SelectedItem is FlowDirection flowDirection)
+ {
+ this.FlowDirection = flowDirection;
+ }
+ };
+
var decorations = this.Find("Decorations");
decorations.SelectionChanged += (sender, e) =>
{
diff --git a/samples/ControlCatalog/Pages/ButtonsPage.xaml b/samples/ControlCatalog/Pages/ButtonsPage.xaml
index 7de0872eae..059b4d9788 100644
--- a/samples/ControlCatalog/Pages/ButtonsPage.xaml
+++ b/samples/ControlCatalog/Pages/ButtonsPage.xaml
@@ -147,6 +147,35 @@
+
+
+
+
+ A button with an added drop-down chevron to visually indicate it has a flyout with additional actions.
+
+
+
+
+
+
+
+
+
+
+
+ Disabled
+
+
+
+
+ Content="Re-themed"
+ Foreground="White">
@@ -151,7 +151,7 @@
BorderThickness="{TemplateBinding BorderThickness}"
CornerRadius="{TemplateBinding CornerRadius}"
Content="{TemplateBinding Content}"
- TextBlock.Foreground="{TemplateBinding Foreground}"
+ TextElement.Foreground="{TemplateBinding Foreground}"
HorizontalContentAlignment="Stretch"
VerticalContentAlignment="Stretch"/>
@@ -196,7 +196,7 @@
diff --git a/src/Avalonia.Themes.Default/Controls/DropDownButton.xaml b/src/Avalonia.Themes.Default/Controls/DropDownButton.xaml
new file mode 100644
index 0000000000..662c6379fb
--- /dev/null
+++ b/src/Avalonia.Themes.Default/Controls/DropDownButton.xaml
@@ -0,0 +1,67 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Avalonia.Themes.Default/Controls/RepeatButton.xaml b/src/Avalonia.Themes.Default/Controls/RepeatButton.xaml
index a9a03c8ed5..47398966f7 100644
--- a/src/Avalonia.Themes.Default/Controls/RepeatButton.xaml
+++ b/src/Avalonia.Themes.Default/Controls/RepeatButton.xaml
@@ -24,7 +24,7 @@
ContentTemplate="{TemplateBinding ContentTemplate}"
Content="{TemplateBinding Content}"
Padding="{TemplateBinding Padding}"
- TextBlock.Foreground="{TemplateBinding Foreground}"
+ TextElement.Foreground="{TemplateBinding Foreground}"
HorizontalContentAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalContentAlignment="{TemplateBinding VerticalContentAlignment}"/>
diff --git a/src/Avalonia.Themes.Default/Controls/SplitButton.xaml b/src/Avalonia.Themes.Default/Controls/SplitButton.xaml
index ce20a1a165..0c46ce3724 100644
--- a/src/Avalonia.Themes.Default/Controls/SplitButton.xaml
+++ b/src/Avalonia.Themes.Default/Controls/SplitButton.xaml
@@ -16,6 +16,7 @@
24241
+ 241
@@ -59,8 +60,11 @@
+
+
+
@@ -146,7 +150,7 @@
SplitButton /template/ Button#PART_SecondaryButton:pointerover /template/ ContentPresenter">
-
+
diff --git a/src/Avalonia.Themes.Default/Controls/ToggleButton.xaml b/src/Avalonia.Themes.Default/Controls/ToggleButton.xaml
index b14a239c35..17fb2af16c 100644
--- a/src/Avalonia.Themes.Default/Controls/ToggleButton.xaml
+++ b/src/Avalonia.Themes.Default/Controls/ToggleButton.xaml
@@ -18,7 +18,7 @@
Content="{TemplateBinding Content}"
Padding="{TemplateBinding Padding}"
RecognizesAccessKey="True"
- TextBlock.Foreground="{TemplateBinding Foreground}"
+ TextElement.Foreground="{TemplateBinding Foreground}"
HorizontalContentAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalContentAlignment="{TemplateBinding VerticalContentAlignment}"/>
diff --git a/src/Avalonia.Themes.Default/DefaultTheme.xaml b/src/Avalonia.Themes.Default/DefaultTheme.xaml
index 846d45b839..468b723f5b 100644
--- a/src/Avalonia.Themes.Default/DefaultTheme.xaml
+++ b/src/Avalonia.Themes.Default/DefaultTheme.xaml
@@ -14,6 +14,7 @@
+
diff --git a/src/Avalonia.Themes.Fluent/Controls/Button.xaml b/src/Avalonia.Themes.Fluent/Controls/Button.xaml
index 533fabfb44..f545206a2f 100644
--- a/src/Avalonia.Themes.Fluent/Controls/Button.xaml
+++ b/src/Avalonia.Themes.Fluent/Controls/Button.xaml
@@ -4,7 +4,7 @@
-
+
@@ -19,9 +19,7 @@
-
-
-
+
@@ -45,40 +43,40 @@
-
+
-
+
-
-
diff --git a/src/Avalonia.Themes.Fluent/Controls/CalendarItem.xaml b/src/Avalonia.Themes.Fluent/Controls/CalendarItem.xaml
index a9c8281cf0..e8e2df03b4 100644
--- a/src/Avalonia.Themes.Fluent/Controls/CalendarItem.xaml
+++ b/src/Avalonia.Themes.Fluent/Controls/CalendarItem.xaml
@@ -61,17 +61,17 @@
diff --git a/src/Avalonia.Themes.Fluent/Controls/DataValidationErrors.xaml b/src/Avalonia.Themes.Fluent/Controls/DataValidationErrors.xaml
index 12e148d2f9..649a186c7e 100644
--- a/src/Avalonia.Themes.Fluent/Controls/DataValidationErrors.xaml
+++ b/src/Avalonia.Themes.Fluent/Controls/DataValidationErrors.xaml
@@ -29,7 +29,7 @@
@@ -165,7 +165,7 @@
Background="{TemplateBinding Background}"
BorderThickness="{TemplateBinding BorderThickness}"
Content="{TemplateBinding Content}"
- TextBlock.Foreground="{TemplateBinding Foreground}"
+ Foreground="{TemplateBinding Foreground}"
HorizontalContentAlignment="Stretch"
VerticalContentAlignment="Stretch"
CornerRadius="{TemplateBinding CornerRadius}"/>
@@ -212,7 +212,7 @@
diff --git a/src/Avalonia.Themes.Fluent/Controls/DropDownButton.xaml b/src/Avalonia.Themes.Fluent/Controls/DropDownButton.xaml
new file mode 100644
index 0000000000..b96c689ab6
--- /dev/null
+++ b/src/Avalonia.Themes.Fluent/Controls/DropDownButton.xaml
@@ -0,0 +1,103 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ 32
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Avalonia.Themes.Fluent/Controls/Expander.xaml b/src/Avalonia.Themes.Fluent/Controls/Expander.xaml
index c5bc7b6a38..33d502772e 100644
--- a/src/Avalonia.Themes.Fluent/Controls/Expander.xaml
+++ b/src/Avalonia.Themes.Fluent/Controls/Expander.xaml
@@ -110,7 +110,7 @@
BorderThickness="0"
Content="{TemplateBinding Content}"
ContentTemplate="{TemplateBinding ContentTemplate}"
- TextBlock.Foreground="{DynamicResource ExpanderForeground}" />
+ Foreground="{DynamicResource ExpanderForeground}" />
+
diff --git a/src/Avalonia.Themes.Fluent/Controls/ListBox.xaml b/src/Avalonia.Themes.Fluent/Controls/ListBox.xaml
index e41e1b5fde..8011ed9daf 100644
--- a/src/Avalonia.Themes.Fluent/Controls/ListBox.xaml
+++ b/src/Avalonia.Themes.Fluent/Controls/ListBox.xaml
@@ -10,7 +10,7 @@
@@ -49,7 +49,7 @@
@@ -57,7 +57,7 @@
@@ -65,7 +65,7 @@
@@ -73,7 +73,7 @@
@@ -81,7 +81,7 @@
@@ -89,6 +89,6 @@
diff --git a/src/Avalonia.Themes.Fluent/Controls/MenuItem.xaml b/src/Avalonia.Themes.Fluent/Controls/MenuItem.xaml
index 831537f578..33cf6bfdde 100644
--- a/src/Avalonia.Themes.Fluent/Controls/MenuItem.xaml
+++ b/src/Avalonia.Themes.Fluent/Controls/MenuItem.xaml
@@ -218,7 +218,7 @@
diff --git a/src/Avalonia.Themes.Fluent/Controls/Slider.xaml b/src/Avalonia.Themes.Fluent/Controls/Slider.xaml
index 5ee2dc527f..cd2c02c567 100644
--- a/src/Avalonia.Themes.Fluent/Controls/Slider.xaml
+++ b/src/Avalonia.Themes.Fluent/Controls/Slider.xaml
@@ -45,7 +45,7 @@
-
@@ -107,7 +107,7 @@
-
@@ -208,7 +208,7 @@
diff --git a/src/Avalonia.Themes.Fluent/Controls/TabStripItem.xaml b/src/Avalonia.Themes.Fluent/Controls/TabStripItem.xaml
index 694e9ef579..59f68a1547 100644
--- a/src/Avalonia.Themes.Fluent/Controls/TabStripItem.xaml
+++ b/src/Avalonia.Themes.Fluent/Controls/TabStripItem.xaml
@@ -36,9 +36,9 @@
Content="{TemplateBinding Content}"
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
- TextBlock.FontFamily="{TemplateBinding FontFamily}"
- TextBlock.FontSize="{TemplateBinding FontSize}"
- TextBlock.FontWeight="{TemplateBinding FontWeight}" />
+ FontFamily="{TemplateBinding FontFamily}"
+ FontSize="{TemplateBinding FontSize}"
+ FontWeight="{TemplateBinding FontWeight}" />
@@ -69,7 +69,7 @@
diff --git a/src/Avalonia.Themes.Fluent/Controls/TimePicker.xaml b/src/Avalonia.Themes.Fluent/Controls/TimePicker.xaml
index 9aa73fc52e..e03dc76f16 100644
--- a/src/Avalonia.Themes.Fluent/Controls/TimePicker.xaml
+++ b/src/Avalonia.Themes.Fluent/Controls/TimePicker.xaml
@@ -51,7 +51,7 @@
ContentTemplate="{TemplateBinding HeaderTemplate}"
Margin="{DynamicResource TimePickerTopHeaderMargin}"
MaxWidth="{DynamicResource TimePickerThemeMaxWidth}"
- TextBlock.Foreground="{DynamicResource TimePickerHeaderForeground}"
+ Foreground="{DynamicResource TimePickerHeaderForeground}"
HorizontalAlignment="Stretch"
VerticalAlignment="Top" />
@@ -77,7 +77,7 @@
BorderThickness="{TemplateBinding BorderThickness}"
CornerRadius="{TemplateBinding CornerRadius}"
Content="{TemplateBinding Content}"
- TextBlock.Foreground="{TemplateBinding Foreground}"
+ Foreground="{TemplateBinding Foreground}"
HorizontalContentAlignment="Stretch"
VerticalContentAlignment="Stretch" />
@@ -139,7 +139,7 @@
diff --git a/src/Avalonia.Themes.Fluent/Controls/TreeViewItem.xaml b/src/Avalonia.Themes.Fluent/Controls/TreeViewItem.xaml
index e66392113f..f86b67bb6c 100644
--- a/src/Avalonia.Themes.Fluent/Controls/TreeViewItem.xaml
+++ b/src/Avalonia.Themes.Fluent/Controls/TreeViewItem.xaml
@@ -107,7 +107,7 @@
@@ -116,7 +116,7 @@
@@ -125,7 +125,7 @@
@@ -134,7 +134,7 @@
@@ -143,7 +143,7 @@
@@ -152,7 +152,7 @@
@@ -161,7 +161,7 @@
diff --git a/src/Avalonia.Themes.Fluent/Controls/WindowNotificationManager.xaml b/src/Avalonia.Themes.Fluent/Controls/WindowNotificationManager.xaml
index 2ff5284d0d..8d14c2d972 100644
--- a/src/Avalonia.Themes.Fluent/Controls/WindowNotificationManager.xaml
+++ b/src/Avalonia.Themes.Fluent/Controls/WindowNotificationManager.xaml
@@ -9,7 +9,7 @@
-
+
diff --git a/src/Avalonia.Visuals/Animation/CompositePageTransition.cs b/src/Avalonia.Visuals/Animation/CompositePageTransition.cs
index 2a6eae3e38..62119a0051 100644
--- a/src/Avalonia.Visuals/Animation/CompositePageTransition.cs
+++ b/src/Avalonia.Visuals/Animation/CompositePageTransition.cs
@@ -41,7 +41,7 @@ namespace Avalonia.Animation
{
var transitionTasks = PageTransitions
.Select(transition => transition.Start(from, to, forward, cancellationToken))
- .ToList();
+ .ToArray();
return Task.WhenAll(transitionTasks);
}
}
diff --git a/src/Avalonia.Visuals/ApiCompatBaseline.txt b/src/Avalonia.Visuals/ApiCompatBaseline.txt
index b725993b44..0e82ef7367 100644
--- a/src/Avalonia.Visuals/ApiCompatBaseline.txt
+++ b/src/Avalonia.Visuals/ApiCompatBaseline.txt
@@ -67,6 +67,8 @@ MembersMustExist : Member 'public void Avalonia.Media.Immutable.ImmutableRadialG
TypeCannotChangeClassification : Type 'Avalonia.Media.Immutable.ImmutableSolidColorBrush' is a 'class' in the implementation but is a 'struct' in the contract.
MembersMustExist : Member 'public void Avalonia.Media.Immutable.ImmutableSolidColorBrush..ctor(Avalonia.Media.Color, System.Double)' does not exist in the implementation but it does exist in the contract.
MembersMustExist : Member 'protected void Avalonia.Media.Immutable.ImmutableTileBrush..ctor(Avalonia.Media.AlignmentX, Avalonia.Media.AlignmentY, Avalonia.RelativeRect, System.Double, Avalonia.RelativeRect, Avalonia.Media.Stretch, Avalonia.Media.TileMode, Avalonia.Visuals.Media.Imaging.BitmapInterpolationMode)' does not exist in the implementation but it does exist in the contract.
+CannotAddAbstractMembers : Member 'public System.Double Avalonia.Media.TextFormatting.DrawableTextRun.Baseline' is abstract in the implementation but is missing in the contract.
+CannotAddAbstractMembers : Member 'public System.Double Avalonia.Media.TextFormatting.DrawableTextRun.Baseline.get()' is abstract in the implementation but is missing in the contract.
MembersMustExist : Member 'public void Avalonia.Media.TextFormatting.DrawableTextRun.Draw(Avalonia.Media.DrawingContext)' does not exist in the implementation but it does exist in the contract.
CannotAddAbstractMembers : Member 'public void Avalonia.Media.TextFormatting.DrawableTextRun.Draw(Avalonia.Media.DrawingContext, Avalonia.Point)' is abstract in the implementation but is missing in the contract.
CannotSealType : Type 'Avalonia.Media.TextFormatting.GenericTextParagraphProperties' is actually (has the sealed modifier) sealed in the implementation but not sealed in the contract.
@@ -85,7 +87,7 @@ MembersMustExist : Member 'public void Avalonia.Media.TextFormatting.ShapedTextC
MembersMustExist : Member 'public Avalonia.Media.TextFormatting.ShapedTextCharacters.SplitTextCharactersResult Avalonia.Media.TextFormatting.ShapedTextCharacters.Split(System.Int32)' does not exist in the implementation but it does exist in the contract.
TypesMustExist : Type 'Avalonia.Media.TextFormatting.ShapedTextCharacters.SplitTextCharactersResult' does not exist in the implementation but it does exist in the contract.
MembersMustExist : Member 'protected System.Boolean Avalonia.Media.TextFormatting.TextCharacters.TryGetRunProperties(Avalonia.Utilities.ReadOnlySlice, Avalonia.Media.Typeface, Avalonia.Media.Typeface, System.Int32)' does not exist in the implementation but it does exist in the contract.
-CannotAddAbstractMembers : Member 'public System.Collections.Generic.IReadOnlyList Avalonia.Media.TextFormatting.TextCollapsingProperties.Collapse(Avalonia.Media.TextFormatting.TextLine)' is abstract in the implementation but is missing in the contract.
+CannotAddAbstractMembers : Member 'public System.Collections.Generic.List Avalonia.Media.TextFormatting.TextCollapsingProperties.Collapse(Avalonia.Media.TextFormatting.TextLine)' is abstract in the implementation but is missing in the contract.
MembersMustExist : Member 'public Avalonia.Media.TextFormatting.TextCollapsingStyle Avalonia.Media.TextFormatting.TextCollapsingProperties.Style.get()' does not exist in the implementation but it does exist in the contract.
TypesMustExist : Type 'Avalonia.Media.TextFormatting.TextCollapsingStyle' does not exist in the implementation but it does exist in the contract.
MembersMustExist : Member 'public void Avalonia.Media.TextFormatting.TextEndOfLine..ctor()' does not exist in the implementation but it does exist in the contract.
@@ -94,8 +96,10 @@ MembersMustExist : Member 'public void Avalonia.Media.TextFormatting.TextLayout.
MembersMustExist : Member 'public Avalonia.Size Avalonia.Media.TextFormatting.TextLayout.Size.get()' does not exist in the implementation but it does exist in the contract.
CannotAddAbstractMembers : Member 'public System.Double Avalonia.Media.TextFormatting.TextLine.Baseline' is abstract in the implementation but is missing in the contract.
CannotAddAbstractMembers : Member 'public System.Double Avalonia.Media.TextFormatting.TextLine.Extent' is abstract in the implementation but is missing in the contract.
+CannotAddAbstractMembers : Member 'public System.Int32 Avalonia.Media.TextFormatting.TextLine.FirstTextSourceIndex' is abstract in the implementation but is missing in the contract.
CannotAddAbstractMembers : Member 'public System.Boolean Avalonia.Media.TextFormatting.TextLine.HasOverflowed' is abstract in the implementation but is missing in the contract.
CannotAddAbstractMembers : Member 'public System.Double Avalonia.Media.TextFormatting.TextLine.Height' is abstract in the implementation but is missing in the contract.
+CannotAddAbstractMembers : Member 'public System.Int32 Avalonia.Media.TextFormatting.TextLine.Length' is abstract in the implementation but is missing in the contract.
CannotAddAbstractMembers : Member 'public System.Int32 Avalonia.Media.TextFormatting.TextLine.NewLineLength' is abstract in the implementation but is missing in the contract.
CannotAddAbstractMembers : Member 'public System.Double Avalonia.Media.TextFormatting.TextLine.OverhangAfter' is abstract in the implementation but is missing in the contract.
CannotAddAbstractMembers : Member 'public System.Double Avalonia.Media.TextFormatting.TextLine.OverhangLeading' is abstract in the implementation but is missing in the contract.
@@ -108,18 +112,23 @@ CannotAddAbstractMembers : Member 'public System.Double Avalonia.Media.TextForma
MembersMustExist : Member 'public void Avalonia.Media.TextFormatting.TextLine.Draw(Avalonia.Media.DrawingContext)' does not exist in the implementation but it does exist in the contract.
CannotAddAbstractMembers : Member 'public void Avalonia.Media.TextFormatting.TextLine.Draw(Avalonia.Media.DrawingContext, Avalonia.Point)' is abstract in the implementation but is missing in the contract.
CannotAddAbstractMembers : Member 'public System.Double Avalonia.Media.TextFormatting.TextLine.Extent.get()' is abstract in the implementation but is missing in the contract.
+CannotAddAbstractMembers : Member 'public System.Int32 Avalonia.Media.TextFormatting.TextLine.FirstTextSourceIndex.get()' is abstract in the implementation but is missing in the contract.
+CannotAddAbstractMembers : Member 'public System.Collections.Generic.IReadOnlyList Avalonia.Media.TextFormatting.TextLine.GetTextBounds(System.Int32, System.Int32)' is abstract in the implementation but is missing in the contract.
CannotAddAbstractMembers : Member 'public System.Boolean Avalonia.Media.TextFormatting.TextLine.HasOverflowed.get()' is abstract in the implementation but is missing in the contract.
CannotAddAbstractMembers : Member 'public System.Double Avalonia.Media.TextFormatting.TextLine.Height.get()' is abstract in the implementation but is missing in the contract.
+CannotAddAbstractMembers : Member 'public System.Int32 Avalonia.Media.TextFormatting.TextLine.Length.get()' is abstract in the implementation but is missing in the contract.
MembersMustExist : Member 'public Avalonia.Media.TextFormatting.TextLineMetrics Avalonia.Media.TextFormatting.TextLine.LineMetrics.get()' does not exist in the implementation but it does exist in the contract.
CannotAddAbstractMembers : Member 'public System.Int32 Avalonia.Media.TextFormatting.TextLine.NewLineLength.get()' is abstract in the implementation but is missing in the contract.
CannotAddAbstractMembers : Member 'public System.Double Avalonia.Media.TextFormatting.TextLine.OverhangAfter.get()' is abstract in the implementation but is missing in the contract.
CannotAddAbstractMembers : Member 'public System.Double Avalonia.Media.TextFormatting.TextLine.OverhangLeading.get()' is abstract in the implementation but is missing in the contract.
CannotAddAbstractMembers : Member 'public System.Double Avalonia.Media.TextFormatting.TextLine.OverhangTrailing.get()' is abstract in the implementation but is missing in the contract.
CannotAddAbstractMembers : Member 'public System.Double Avalonia.Media.TextFormatting.TextLine.Start.get()' is abstract in the implementation but is missing in the contract.
+MembersMustExist : Member 'public Avalonia.Media.TextFormatting.TextRange Avalonia.Media.TextFormatting.TextLine.TextRange.get()' does not exist in the implementation but it does exist in the contract.
CannotAddAbstractMembers : Member 'public System.Int32 Avalonia.Media.TextFormatting.TextLine.TrailingWhitespaceLength.get()' is abstract in the implementation but is missing in the contract.
CannotAddAbstractMembers : Member 'public System.Double Avalonia.Media.TextFormatting.TextLine.Width.get()' is abstract in the implementation but is missing in the contract.
CannotAddAbstractMembers : Member 'public System.Double Avalonia.Media.TextFormatting.TextLine.WidthIncludingTrailingWhitespace.get()' is abstract in the implementation but is missing in the contract.
MembersMustExist : Member 'public void Avalonia.Media.TextFormatting.TextLineBreak..ctor(System.Collections.Generic.IReadOnlyList)' does not exist in the implementation but it does exist in the contract.
+MembersMustExist : Member 'public System.Collections.Generic.IReadOnlyList Avalonia.Media.TextFormatting.TextLineBreak.RemainingCharacters.get()' does not exist in the implementation but it does exist in the contract.
MembersMustExist : Member 'public void Avalonia.Media.TextFormatting.TextLineMetrics..ctor(Avalonia.Size, System.Double, Avalonia.Media.TextFormatting.TextRange, System.Boolean)' does not exist in the implementation but it does exist in the contract.
MembersMustExist : Member 'public Avalonia.Media.TextFormatting.TextLineMetrics Avalonia.Media.TextFormatting.TextLineMetrics.Create(System.Collections.Generic.IEnumerable, Avalonia.Media.TextFormatting.TextRange, System.Double, Avalonia.Media.TextFormatting.TextParagraphProperties)' does not exist in the implementation but it does exist in the contract.
MembersMustExist : Member 'public Avalonia.Size Avalonia.Media.TextFormatting.TextLineMetrics.Size.get()' does not exist in the implementation but it does exist in the contract.
@@ -130,8 +139,6 @@ CannotAddAbstractMembers : Member 'public System.Double Avalonia.Media.TextForma
CannotAddAbstractMembers : Member 'public System.Boolean Avalonia.Media.TextFormatting.TextParagraphProperties.FirstLineInParagraph.get()' is abstract in the implementation but is missing in the contract.
CannotAddAbstractMembers : Member 'public Avalonia.Media.FlowDirection Avalonia.Media.TextFormatting.TextParagraphProperties.FlowDirection.get()' is abstract in the implementation but is missing in the contract.
CannotAddAbstractMembers : Member 'public System.Double Avalonia.Media.TextFormatting.TextParagraphProperties.Indent.get()' is abstract in the implementation but is missing in the contract.
-CannotAddAbstractMembers : Member 'public Avalonia.Media.BaselineAlignment Avalonia.Media.TextFormatting.TextRunProperties.BaselineAlignment' is abstract in the implementation but is missing in the contract.
-CannotAddAbstractMembers : Member 'public Avalonia.Media.BaselineAlignment Avalonia.Media.TextFormatting.TextRunProperties.BaselineAlignment.get()' is abstract in the implementation but is missing in the contract.
MembersMustExist : Member 'public Avalonia.Media.GlyphRun Avalonia.Media.TextFormatting.TextShaper.ShapeText(Avalonia.Utilities.ReadOnlySlice, Avalonia.Media.Typeface, System.Double, System.Globalization.CultureInfo)' does not exist in the implementation but it does exist in the contract.
CannotSealType : Type 'Avalonia.Media.TextFormatting.TextTrailingCharacterEllipsis' is actually (has the sealed modifier) sealed in the implementation but not sealed in the contract.
MembersMustExist : Member 'public void Avalonia.Media.TextFormatting.TextTrailingCharacterEllipsis..ctor(System.Double, Avalonia.Media.TextFormatting.TextRunProperties)' does not exist in the implementation but it does exist in the contract.
@@ -171,9 +178,11 @@ InterfacesShouldHaveSameMembers : Interface member 'public Avalonia.Size Avaloni
InterfacesShouldHaveSameMembers : Interface member 'public System.TimeSpan Avalonia.Platform.IPlatformSettings.TouchDoubleClickTime' is present in the implementation but not in the contract.
InterfacesShouldHaveSameMembers : Interface member 'public Avalonia.Size Avalonia.Platform.IPlatformSettings.TouchDoubleClickSize.get()' is present in the implementation but not in the contract.
InterfacesShouldHaveSameMembers : Interface member 'public System.TimeSpan Avalonia.Platform.IPlatformSettings.TouchDoubleClickTime.get()' is present in the implementation but not in the contract.
-InterfacesShouldHaveSameMembers : Interface member 'public Avalonia.Media.TextFormatting.ShapedBuffer Avalonia.Platform.ITextShaperImpl.ShapeText(Avalonia.Utilities.ReadOnlySlice, Avalonia.Media.GlyphTypeface, System.Double, System.Globalization.CultureInfo, System.SByte)' is present in the implementation but not in the contract.
+InterfacesShouldHaveSameMembers : Interface member 'public Avalonia.Media.TextFormatting.ShapedBuffer Avalonia.Platform.ITextShaperImpl.ShapeText(Avalonia.Utilities.ReadOnlySlice, Avalonia.Media.TextFormatting.TextShaperOptions)' is present in the implementation but not in the contract.
InterfacesShouldHaveSameMembers : Interface member 'public Avalonia.Media.GlyphRun Avalonia.Platform.ITextShaperImpl.ShapeText(Avalonia.Utilities.ReadOnlySlice, Avalonia.Media.Typeface, System.Double, System.Globalization.CultureInfo)' is present in the contract but not in the implementation.
MembersMustExist : Member 'public Avalonia.Media.GlyphRun Avalonia.Platform.ITextShaperImpl.ShapeText(Avalonia.Utilities.ReadOnlySlice, Avalonia.Media.Typeface, System.Double, System.Globalization.CultureInfo)' does not exist in the implementation but it does exist in the contract.
MembersMustExist : Member 'protected void Avalonia.Rendering.RendererBase.RenderFps(Avalonia.Platform.IDrawingContextImpl, Avalonia.Rect, System.Nullable)' does not exist in the implementation but it does exist in the contract.
+InterfacesShouldHaveSameMembers : Interface member 'public System.Boolean Avalonia.VisualTree.IVisual.HasMirrorTransform' is present in the implementation but not in the contract.
+InterfacesShouldHaveSameMembers : Interface member 'public System.Boolean Avalonia.VisualTree.IVisual.HasMirrorTransform.get()' is present in the implementation but not in the contract.
MembersMustExist : Member 'public void Avalonia.Utilities.ReadOnlySlice..ctor(System.ReadOnlyMemory, System.Int32, System.Int32)' does not exist in the implementation but it does exist in the contract.
-Total Issues: 177
+Total Issues: 186
diff --git a/src/Avalonia.Visuals/Avalonia.Visuals.csproj b/src/Avalonia.Visuals/Avalonia.Visuals.csproj
index a0b1f99fa1..7e9947812b 100644
--- a/src/Avalonia.Visuals/Avalonia.Visuals.csproj
+++ b/src/Avalonia.Visuals/Avalonia.Visuals.csproj
@@ -19,4 +19,5 @@
+
diff --git a/src/Avalonia.Visuals/Media/Color.cs b/src/Avalonia.Visuals/Media/Color.cs
index e974bbb100..eaa886ccbd 100644
--- a/src/Avalonia.Visuals/Media/Color.cs
+++ b/src/Avalonia.Visuals/Media/Color.cs
@@ -1,3 +1,10 @@
+// Color conversion portions of this source file are adapted from the WinUI project
+// (https://github.com/microsoft/microsoft-ui-xaml)
+// and the Windows Community Toolkit project.
+// (https://github.com/CommunityToolkit/WindowsCommunityToolkit)
+//
+// Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation.
+
using System;
using System.Globalization;
#if !BUILDTASK
@@ -14,6 +21,8 @@ namespace Avalonia.Media
#endif
readonly struct Color : IEquatable
{
+ private const double byteToDouble = 1.0 / 255;
+
static Color()
{
#if !BUILDTASK
@@ -41,6 +50,13 @@ namespace Avalonia.Media
///
public byte B { get; }
+ ///
+ /// Initializes a new instance of the struct.
+ ///
+ /// The alpha component.
+ /// The red component.
+ /// The green component.
+ /// The blue component.
public Color(byte a, byte r, byte g, byte b)
{
A = a;
@@ -144,17 +160,46 @@ namespace Avalonia.Media
return false;
}
- if (s[0] == '#' && TryParseInternal(s.AsSpan(), out color))
+ if (s[0] == '#' &&
+ TryParseHexFormat(s.AsSpan(), out color))
{
return true;
}
+ if (s.Length > 5 &&
+ (s[0] == 'r' || s[0] == 'R') &&
+ (s[1] == 'g' || s[1] == 'G') &&
+ (s[2] == 'b' || s[2] == 'B') &&
+ TryParseCssFormat(s, out color))
+ {
+ return true;
+ }
+
+ if (s.Length > 5 &&
+ (s[0] == 'h' || s[0] == 'H') &&
+ (s[1] == 's' || s[1] == 'S') &&
+ (s[2] == 'l' || s[2] == 'L') &&
+ HslColor.TryParse(s, out HslColor hslColor))
+ {
+ color = hslColor.ToRgb();
+ return true;
+ }
+
+ if (s.Length > 5 &&
+ (s[0] == 'h' || s[0] == 'H') &&
+ (s[1] == 's' || s[1] == 'S') &&
+ (s[2] == 'v' || s[2] == 'V') &&
+ HsvColor.TryParse(s, out HsvColor hsvColor))
+ {
+ color = hsvColor.ToRgb();
+ return true;
+ }
+
var knownColor = KnownColors.GetKnownColor(s);
if (knownColor != KnownColor.None)
{
color = knownColor.ToColor();
-
return true;
}
@@ -172,21 +217,52 @@ namespace Avalonia.Media
if (s.Length == 0)
{
color = default;
-
return false;
}
- if (s[0] == '#')
+ if (s[0] == '#' &&
+ TryParseHexFormat(s, out color))
+ {
+ return true;
+ }
+
+ // At this point all parsing uses strings
+ var str = s.ToString();
+
+ if (s.Length > 5 &&
+ (s[0] == 'r' || s[0] == 'R') &&
+ (s[1] == 'g' || s[1] == 'G') &&
+ (s[2] == 'b' || s[2] == 'B') &&
+ TryParseCssFormat(str, out color))
+ {
+ return true;
+ }
+
+ if (s.Length > 5 &&
+ (s[0] == 'h' || s[0] == 'H') &&
+ (s[1] == 's' || s[1] == 'S') &&
+ (s[2] == 'l' || s[2] == 'L') &&
+ HslColor.TryParse(str, out HslColor hslColor))
{
- return TryParseInternal(s, out color);
+ color = hslColor.ToRgb();
+ return true;
}
- var knownColor = KnownColors.GetKnownColor(s.ToString());
+ if (s.Length > 5 &&
+ (s[0] == 'h' || s[0] == 'H') &&
+ (s[1] == 's' || s[1] == 'S') &&
+ (s[2] == 'v' || s[2] == 'V') &&
+ HsvColor.TryParse(str, out HsvColor hsvColor))
+ {
+ color = hsvColor.ToRgb();
+ return true;
+ }
+
+ var knownColor = KnownColors.GetKnownColor(str);
if (knownColor != KnownColor.None)
{
color = knownColor.ToColor();
-
return true;
}
@@ -195,7 +271,7 @@ namespace Avalonia.Media
return false;
}
- private static bool TryParseInternal(ReadOnlySpan s, out Color color)
+ private static bool TryParseHexFormat(ReadOnlySpan s, out Color color)
{
static bool TryParseCore(ReadOnlySpan input, ref Color color)
{
@@ -249,6 +325,91 @@ namespace Avalonia.Media
return TryParseCore(input, ref color);
}
+ private static bool TryParseCssFormat(string s, out Color color)
+ {
+ color = default;
+
+ if (s is null)
+ {
+ return false;
+ }
+
+ string workingString = s.Trim();
+
+ if (workingString.Length == 0 ||
+ workingString.IndexOf(",", StringComparison.Ordinal) < 0)
+ {
+ return false;
+ }
+
+ if (workingString.Length > 6 &&
+ workingString.StartsWith("rgba(", StringComparison.OrdinalIgnoreCase) &&
+ workingString.EndsWith(")", StringComparison.Ordinal))
+ {
+ workingString = workingString.Substring(5, workingString.Length - 6);
+ }
+
+ if (workingString.Length > 5 &&
+ workingString.StartsWith("rgb(", StringComparison.OrdinalIgnoreCase) &&
+ workingString.EndsWith(")", StringComparison.Ordinal))
+ {
+ workingString = workingString.Substring(4, workingString.Length - 5);
+ }
+
+ string[] components = workingString.Split(',');
+
+ if (components.Length == 3) // RGB
+ {
+ if (byte.TryParse(components[0], NumberStyles.Number, CultureInfo.InvariantCulture, out byte red) &&
+ byte.TryParse(components[1], NumberStyles.Number, CultureInfo.InvariantCulture, out byte green) &&
+ byte.TryParse(components[2], NumberStyles.Number, CultureInfo.InvariantCulture, out byte blue))
+ {
+ color = new Color(0xFF, red, green, blue);
+ return true;
+ }
+ }
+ else if (components.Length == 4) // RGBA
+ {
+ if (byte.TryParse(components[0], NumberStyles.Number, CultureInfo.InvariantCulture, out byte red) &&
+ byte.TryParse(components[1], NumberStyles.Number, CultureInfo.InvariantCulture, out byte green) &&
+ byte.TryParse(components[2], NumberStyles.Number, CultureInfo.InvariantCulture, out byte blue) &&
+ TryInternalParse(components[3], out double alpha))
+ {
+ color = new Color((byte)(alpha * 255), red, green, blue);
+ return true;
+ }
+ }
+
+ // Local function to specially parse a double value with an optional percentage sign
+ bool TryInternalParse(string inString, out double outDouble)
+ {
+ // The percent sign, if it exists, must be at the end of the number
+ int percentIndex = inString.IndexOf("%", StringComparison.Ordinal);
+
+ if (percentIndex >= 0)
+ {
+ var result = double.TryParse(
+ inString.Substring(0, percentIndex),
+ NumberStyles.Number,
+ CultureInfo.InvariantCulture,
+ out double percentage);
+
+ outDouble = percentage / 100.0;
+ return result;
+ }
+ else
+ {
+ return double.TryParse(
+ inString,
+ NumberStyles.Number,
+ CultureInfo.InvariantCulture,
+ out outDouble);
+ }
+ }
+
+ return false;
+ }
+
///
/// Returns the string representation of the color.
///
@@ -258,7 +419,7 @@ namespace Avalonia.Media
public override string ToString()
{
uint rgb = ToUint32();
- return KnownColors.GetKnownColorName(rgb) ?? $"#{rgb:x8}";
+ return KnownColors.GetKnownColorName(rgb) ?? $"#{rgb.ToString("x8", CultureInfo.InvariantCulture)}";
}
///
@@ -272,15 +433,24 @@ namespace Avalonia.Media
return ((uint)A << 24) | ((uint)R << 16) | ((uint)G << 8) | (uint)B;
}
+ ///
+ /// Returns the HSL color model equivalent of this RGB color.
+ ///
+ /// The HSL equivalent color.
+ public HslColor ToHsl()
+ {
+ // Don't use the HslColor(Color) constructor to avoid an extra HslColor
+ return Color.ToHsl(R, G, B, A);
+ }
+
///
/// Returns the HSV color model equivalent of this RGB color.
///
/// The HSV equivalent color.
public HsvColor ToHsv()
{
- // Use the by-channel conversion method directly for performance
// Don't use the HsvColor(Color) constructor to avoid an extra HsvColor
- return HsvColor.FromRgb(R, G, B, A);
+ return Color.ToHsv(R, G, B, A);
}
///
@@ -289,11 +459,13 @@ namespace Avalonia.Media
return A == other.A && R == other.R && G == other.G && B == other.B;
}
+ ///
public override bool Equals(object? obj)
{
return obj is Color other && Equals(other);
}
+ ///
public override int GetHashCode()
{
unchecked
@@ -306,11 +478,261 @@ namespace Avalonia.Media
}
}
+ ///
+ /// Converts the given RGB color to its HSL color equivalent.
+ ///
+ /// The color in the RGB color model.
+ /// A new equivalent to the given RGBA values.
+ public static HslColor ToHsl(Color color)
+ {
+ // Normalize RGBA components into the 0..1 range
+ return Color.ToHsl(
+ (byteToDouble * color.R),
+ (byteToDouble * color.G),
+ (byteToDouble * color.B),
+ (byteToDouble * color.A));
+ }
+
+ ///
+ /// Converts the given RGBA color component values to their HSL color equivalent.
+ ///
+ /// The Red component in the RGB color model.
+ /// The Green component in the RGB color model.
+ /// The Blue component in the RGB color model.
+ /// The Alpha component.
+ /// A new equivalent to the given RGBA values.
+ public static HslColor ToHsl(
+ byte red,
+ byte green,
+ byte blue,
+ byte alpha = 0xFF)
+ {
+ // Normalize RGBA components into the 0..1 range
+ return Color.ToHsl(
+ (byteToDouble * red),
+ (byteToDouble * green),
+ (byteToDouble * blue),
+ (byteToDouble * alpha));
+ }
+
+ ///
+ /// Converts the given RGBA color component values to their HSL color equivalent.
+ ///
+ ///
+ /// Warning: No bounds checks or clamping is done on the input component values.
+ /// This method is for internal-use only and the caller must ensure bounds.
+ ///
+ /// The Red component in the RGB color model within the range 0..1.
+ /// The Green component in the RGB color model within the range 0..1.
+ /// The Blue component in the RGB color model within the range 0..1.
+ /// The Alpha component in the RGB color model within the range 0..1.
+ /// A new equivalent to the given RGBA values.
+ internal static HslColor ToHsl(
+ double r,
+ double g,
+ double b,
+ double a = 1.0)
+ {
+ // Note: Conversion code is originally based on ColorHelper in the Windows Community Toolkit (licensed MIT)
+ // https://github.com/CommunityToolkit/WindowsCommunityToolkit/blob/main/Microsoft.Toolkit.Uwp/Helpers/ColorHelper.cs
+ // It has been modified.
+
+ double max = r >= g ? (r >= b ? r : b) : (g >= b ? g : b);
+ double min = r <= g ? (r <= b ? r : b) : (g <= b ? g : b);
+ double chroma = max - min;
+ double h1;
+
+ if (chroma == 0)
+ {
+ h1 = 0;
+ }
+ else if (max == r)
+ {
+ // The % operator doesn't do proper modulo on negative
+ // numbers, so we'll add 6 before using it
+ h1 = (((g - b) / chroma) + 6) % 6;
+ }
+ else if (max == g)
+ {
+ h1 = 2 + ((b - r) / chroma);
+ }
+ else
+ {
+ h1 = 4 + ((r - g) / chroma);
+ }
+
+ double lightness = 0.5 * (max + min);
+ double saturation = chroma == 0 ? 0 : chroma / (1 - Math.Abs((2 * lightness) - 1));
+
+ return new HslColor(a, 60 * h1, saturation, lightness, clampValues: false);
+ }
+
+ ///
+ /// Converts the given RGB color to its HSV color equivalent.
+ ///
+ /// The color in the RGB color model.
+ /// A new equivalent to the given RGBA values.
+ public static HsvColor ToHsv(Color color)
+ {
+ // Normalize RGBA components into the 0..1 range
+ return Color.ToHsv(
+ (byteToDouble * color.R),
+ (byteToDouble * color.G),
+ (byteToDouble * color.B),
+ (byteToDouble * color.A));
+ }
+
+ ///
+ /// Converts the given RGBA color component values to their HSV color equivalent.
+ ///
+ /// The Red component in the RGB color model.
+ /// The Green component in the RGB color model.
+ /// The Blue component in the RGB color model.
+ /// The Alpha component.
+ /// A new equivalent to the given RGBA values.
+ public static HsvColor ToHsv(
+ byte red,
+ byte green,
+ byte blue,
+ byte alpha = 0xFF)
+ {
+ // Normalize RGBA components into the 0..1 range
+ return Color.ToHsv(
+ (byteToDouble * red),
+ (byteToDouble * green),
+ (byteToDouble * blue),
+ (byteToDouble * alpha));
+ }
+
+ ///
+ /// Converts the given RGBA color component values to their HSV color equivalent.
+ ///
+ ///
+ /// Warning: No bounds checks or clamping is done on the input component values.
+ /// This method is for internal-use only and the caller must ensure bounds.
+ ///
+ /// The Red component in the RGB color model within the range 0..1.
+ /// The Green component in the RGB color model within the range 0..1.
+ /// The Blue component in the RGB color model within the range 0..1.
+ /// The Alpha component in the RGB color model within the range 0..1.
+ /// A new equivalent to the given RGBA values.
+ internal static HsvColor ToHsv(
+ double r,
+ double g,
+ double b,
+ double a = 1.0)
+ {
+ // Note: Conversion code is originally based on the C++ in WinUI (licensed MIT)
+ // https://github.com/microsoft/microsoft-ui-xaml/blob/main/dev/Common/ColorConversion.cpp
+ // This was used because it is the best documented and likely most optimized for performance
+ // Alpha support was added
+
+ double hue;
+ double saturation;
+ double value;
+
+ double max = r >= g ? (r >= b ? r : b) : (g >= b ? g : b);
+ double min = r <= g ? (r <= b ? r : b) : (g <= b ? g : b);
+
+ // The value, a number between 0 and 1, is the largest of R, G, and B (divided by 255).
+ // Conceptually speaking, it represents how much color is present.
+ // If at least one of R, G, B is 255, then there exists as much color as there can be.
+ // If RGB = (0, 0, 0), then there exists no color at all - a value of zero corresponds
+ // to black (i.e., the absence of any color).
+ value = max;
+
+ // The "chroma" of the color is a value directly proportional to the extent to which
+ // the color diverges from greyscale. If, for example, we have RGB = (255, 255, 0),
+ // then the chroma is maximized - this is a pure yellow, no gray of any kind.
+ // On the other hand, if we have RGB = (128, 128, 128), then the chroma being zero
+ // implies that this color is pure greyscale, with no actual hue to be found.
+ var chroma = max - min;
+
+ // If the chrome is zero, then hue is technically undefined - a greyscale color
+ // has no hue. For the sake of convenience, we'll just set hue to zero, since
+ // it will be unused in this circumstance. Since the color is purely gray,
+ // saturation is also equal to zero - you can think of saturation as basically
+ // a measure of hue intensity, such that no hue at all corresponds to a
+ // nonexistent intensity.
+ if (chroma == 0)
+ {
+ hue = 0.0;
+ saturation = 0.0;
+ }
+ else
+ {
+ // In this block, hue is properly defined, so we'll extract both hue
+ // and saturation information from the RGB color.
+
+ // Hue can be thought of as a cyclical thing, between 0 degrees and 360 degrees.
+ // A hue of 0 degrees is red; 120 degrees is green; 240 degrees is blue; and 360 is back to red.
+ // Every other hue is somewhere between either red and green, green and blue, and blue and red,
+ // so every other hue can be thought of as an angle on this color wheel.
+ // These if/else statements determines where on this color wheel our color lies.
+ if (r == max)
+ {
+ // If the red channel is the most pronounced channel, then we exist
+ // somewhere between (-60, 60) on the color wheel - i.e., the section around 0 degrees
+ // where red dominates. We figure out where in that section we are exactly
+ // by considering whether the green or the blue channel is greater - by subtracting green from blue,
+ // then if green is greater, we'll nudge ourselves closer to 60, whereas if blue is greater, then
+ // we'll nudge ourselves closer to -60. We then divide by chroma (which will actually make the result larger,
+ // since chroma is a value between 0 and 1) to normalize the value to ensure that we get the right hue
+ // even if we're very close to greyscale.
+ hue = 60 * (g - b) / chroma;
+ }
+ else if (g == max)
+ {
+ // We do the exact same for the case where the green channel is the most pronounced channel,
+ // only this time we want to see if we should tilt towards the blue direction or the red direction.
+ // We add 120 to center our value in the green third of the color wheel.
+ hue = 120 + (60 * (b - r) / chroma);
+ }
+ else // blue == max
+ {
+ // And we also do the exact same for the case where the blue channel is the most pronounced channel,
+ // only this time we want to see if we should tilt towards the red direction or the green direction.
+ // We add 240 to center our value in the blue third of the color wheel.
+ hue = 240 + (60 * (r - g) / chroma);
+ }
+
+ // Since we want to work within the range [0, 360), we'll add 360 to any value less than zero -
+ // this will bump red values from within -60 to -1 to 300 to 359. The hue is the same at both values.
+ if (hue < 0.0)
+ {
+ hue += 360.0;
+ }
+
+ // The saturation, our final HSV axis, can be thought of as a value between 0 and 1 indicating how intense our color is.
+ // To find it, we divide the chroma - the distance between the minimum and the maximum RGB channels - by the maximum channel (i.e., the value).
+ // This effectively normalizes the chroma - if the maximum is 0.5 and the minimum is 0, the saturation will be (0.5 - 0) / 0.5 = 1,
+ // meaning that although this color is not as bright as it can be, the dark color is as intense as it possibly could be.
+ // If, on the other hand, the maximum is 0.5 and the minimum is 0.25, then the saturation will be (0.5 - 0.25) / 0.5 = 0.5,
+ // meaning that this color is partially washed out.
+ // A saturation value of 0 corresponds to a greyscale color, one in which the color is *completely* washed out and there is no actual hue.
+ saturation = chroma / value;
+ }
+
+ return new HsvColor(a, hue, saturation, value, clampValues: false);
+ }
+
+ ///
+ /// Indicates whether the values of two specified objects are equal.
+ ///
+ /// The first object to compare.
+ /// The second object to compare.
+ /// True if left and right are equal; otherwise, false.
public static bool operator ==(Color left, Color right)
{
return left.Equals(right);
}
+ ///
+ /// Indicates whether the values of two specified objects are not equal.
+ ///
+ /// The first object to compare.
+ /// The second object to compare.
+ /// True if left and right are not equal; otherwise, false.
public static bool operator !=(Color left, Color right)
{
return !left.Equals(right);
diff --git a/src/Avalonia.Visuals/Media/ExperimentalAcrylicMaterial.cs b/src/Avalonia.Visuals/Media/ExperimentalAcrylicMaterial.cs
index cdea5f5fa3..0e485d0db8 100644
--- a/src/Avalonia.Visuals/Media/ExperimentalAcrylicMaterial.cs
+++ b/src/Avalonia.Visuals/Media/ExperimentalAcrylicMaterial.cs
@@ -142,52 +142,6 @@ namespace Avalonia.Media
Color IExperimentalAcrylicMaterial.TintColor => _effectiveTintColor;
- private struct HsvColor
- {
- public float Hue { get; set; }
- public float Saturation { get; set; }
- public float Value { get; set; }
- }
-
- private static HsvColor RgbToHsv(Color color)
- {
- var r = color.R / 255.0f;
- var g = color.G / 255.0f;
- var b = color.B / 255.0f;
- var max = Math.Max(r, Math.Max(g, b));
- var min = Math.Min(r, Math.Min(g, b));
-
- float h, s, v;
- h = v = max;
-
- var d = max - min;
- s = max == 0 ? 0 : d / max;
-
- if (max == min)
- {
- h = 0; // achromatic
- }
- else
- {
- if (max == r)
- {
- h = (g - b) / d + (g < b ? 6 : 0);
- }
- else if (max == g)
- {
- h = (b - r) / d + 2;
- }
- else if (max == b)
- {
- h = (r - g) / d + 4;
- }
-
- h /= 6;
- }
-
- return new HsvColor { Hue = h, Saturation = s, Value = v };
- }
-
private static Color GetEffectiveTintColor(Color tintColor, double tintOpacity)
{
// Update tintColor's alpha with the combined opacity value
@@ -198,7 +152,7 @@ namespace Avalonia.Media
private static double GetTintOpacityModifier(Color tintColor)
{
- // This method supresses the maximum allowable tint opacity depending on the luminosity and saturation of a color by
+ // This method suppresses the maximum allowable tint opacity depending on the luminosity and saturation of a color by
// compressing the range of allowable values - for example, a user-defined value of 100% will be mapped to 45% for pure
// white (100% luminosity), 85% for pure black (0% luminosity), and 90% for pure gray (50% luminosity). The intensity of
// the effect increases linearly as luminosity deviates from 50%. After this effect is calculated, we cancel it out
@@ -210,22 +164,22 @@ namespace Avalonia.Media
const double midPointMaxOpacity = 0.45; // 50% luminosity
const double blackMaxOpacity = 0.45; // 0% luminosity
- var hsv = RgbToHsv(tintColor);
+ var hsv = tintColor.ToHsv();
double opacityModifier = midPointMaxOpacity;
- if (hsv.Value != midPoint)
+ if (hsv.V != midPoint)
{
// Determine maximum suppression amount
double lowestMaxOpacity = midPointMaxOpacity;
double maxDeviation = midPoint;
- if (hsv.Value > midPoint)
+ if (hsv.V > midPoint)
{
lowestMaxOpacity = whiteMaxOpacity; // At white (100% hsvV)
maxDeviation = 1 - maxDeviation;
}
- else if (hsv.Value < midPoint)
+ else if (hsv.V < midPoint)
{
lowestMaxOpacity = blackMaxOpacity; // At black (0% hsvV)
}
@@ -233,14 +187,14 @@ namespace Avalonia.Media
double maxOpacitySuppression = midPointMaxOpacity - lowestMaxOpacity;
// Determine normalized deviation from the midpoint
- double deviation = Math.Abs(hsv.Value - midPoint);
+ double deviation = Math.Abs(hsv.V - midPoint);
double normalizedDeviation = deviation / maxDeviation;
// If we have saturation, reduce opacity suppression to allow that color to come through more
- if (hsv.Saturation > 0)
+ if (hsv.S > 0)
{
// Dampen opacity suppression based on how much saturation there is
- maxOpacitySuppression *= Math.Max(1 - (hsv.Saturation * 2), 0.0);
+ maxOpacitySuppression *= Math.Max(1 - (hsv.S * 2), 0.0);
}
double opacitySuppression = maxOpacitySuppression * normalizedDeviation;
diff --git a/src/Avalonia.Visuals/Media/FormattedText.cs b/src/Avalonia.Visuals/Media/FormattedText.cs
index 1cac3243e3..7bdf59def0 100644
--- a/src/Avalonia.Visuals/Media/FormattedText.cs
+++ b/src/Avalonia.Visuals/Media/FormattedText.cs
@@ -56,7 +56,7 @@ namespace Avalonia.Media
FlowDirection flowDirection,
Typeface typeface,
double emSize,
- IBrush foreground)
+ IBrush? foreground)
{
if (culture is null)
{
@@ -160,7 +160,7 @@ namespace Avalonia.Media
/// Foreground brush
/// The start index of initial character to apply the change to.
/// The number of characters the change should be applied to.
- public void SetForegroundBrush(IBrush foregroundBrush, int startIndex, int count)
+ public void SetForegroundBrush(IBrush? foregroundBrush, int startIndex, int count)
{
var limit = ValidateRange(startIndex, count);
for (var i = startIndex; i < limit;)
@@ -768,7 +768,7 @@ namespace Avalonia.Media
// as a result of the next line measurement
// maybe there is no next line at all
- if (Position + Current.TextRange.Length < _that._text.Length)
+ if (Position + Current.Length < _that._text.Length)
{
bool nextLineFits;
@@ -780,7 +780,7 @@ namespace Avalonia.Media
{
_nextLine = FormatLine(
_textSource,
- Position + Current.TextRange.Length,
+ Position + Current.Length,
MaxLineLength(_lineCount + 1),
_that._defaultParaProps,
currentLineBreak
@@ -819,7 +819,7 @@ namespace Avalonia.Media
_previousHeight = Current.Height;
- Length = Current.TextRange.Length;
+ Length = Current.Length;
_previousLineBreak = currentLineBreak;
@@ -839,17 +839,17 @@ namespace Avalonia.Media
lineBreak
);
- if (_that._trimming != TextTrimming.None && line.HasOverflowed && line.TextRange.Length > 0)
+ if (_that._trimming != TextTrimming.None && line.HasOverflowed && line.Length > 0)
{
// what I really need here is the last displayed text run of the line
// textSourcePosition + line.Length - 1 works except the end of paragraph case,
// where line length includes the fake paragraph break run
- Debug.Assert(_that._text.Length > 0 && textSourcePosition + line.TextRange.Length <= _that._text.Length + 1);
+ Debug.Assert(_that._text.Length > 0 && textSourcePosition + line.Length <= _that._text.Length + 1);
var thatFormatRider = new SpanRider(
_that._formatRuns,
_that._latestPosition,
- Math.Min(textSourcePosition + line.TextRange.Length - 1, _that._text.Length - 1)
+ Math.Min(textSourcePosition + line.Length - 1, _that._text.Length - 1)
);
var lastRunProps = (GenericTextRunProperties)thatFormatRider.CurrentElement!;
diff --git a/src/Avalonia.Visuals/Media/GradientStops.cs b/src/Avalonia.Visuals/Media/GradientStops.cs
index efc11bacd6..c3d0bb5ceb 100644
--- a/src/Avalonia.Visuals/Media/GradientStops.cs
+++ b/src/Avalonia.Visuals/Media/GradientStops.cs
@@ -17,7 +17,7 @@ namespace Avalonia.Media
public IReadOnlyList ToImmutable()
{
- return this.Select(x => new ImmutableGradientStop(x.Offset, x.Color)).ToList();
+ return this.Select(x => new ImmutableGradientStop(x.Offset, x.Color)).ToArray();
}
}
}
diff --git a/src/Avalonia.Visuals/Media/HslColor.cs b/src/Avalonia.Visuals/Media/HslColor.cs
new file mode 100644
index 0000000000..e27a4f3106
--- /dev/null
+++ b/src/Avalonia.Visuals/Media/HslColor.cs
@@ -0,0 +1,476 @@
+// Color conversion portions of this source file are adapted from the Windows Community Toolkit project.
+// (https://github.com/CommunityToolkit/WindowsCommunityToolkit)
+//
+// Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation.
+
+using System;
+using System.Globalization;
+using System.Text;
+using Avalonia.Utilities;
+
+namespace Avalonia.Media
+{
+ ///
+ /// Defines a color using the hue/saturation/lightness (HSL) model.
+ ///
+#if !BUILDTASK
+ public
+#endif
+ readonly struct HslColor : IEquatable
+ {
+ ///
+ /// Initializes a new instance of the struct.
+ ///
+ /// The Alpha (transparency) component in the range from 0..1.
+ /// The Hue component in the range from 0..360.
+ /// Note that 360 is equivalent to 0 and will be adjusted automatically.
+ /// The Saturation component in the range from 0..1.
+ /// The Lightness component in the range from 0..1.
+ public HslColor(
+ double alpha,
+ double hue,
+ double saturation,
+ double lightness)
+ {
+ A = MathUtilities.Clamp(alpha, 0.0, 1.0);
+ H = MathUtilities.Clamp(hue, 0.0, 360.0);
+ S = MathUtilities.Clamp(saturation, 0.0, 1.0);
+ L = MathUtilities.Clamp(lightness, 0.0, 1.0);
+
+ // The maximum value of Hue is technically 360 minus epsilon (just below 360).
+ // This is because, in a color circle, 360 degrees is equivalent to 0 degrees.
+ // However, that is too tricky to work with in code and isn't as intuitive.
+ // Therefore, since 360 == 0, just wrap 360 if needed back to 0.
+ H = (H == 360.0 ? 0 : H);
+ }
+
+ ///
+ /// Initializes a new instance of the struct.
+ ///
+ ///
+ /// This constructor exists only for internal use where performance is critical.
+ /// Whether or not the component values are in the correct ranges must be known.
+ ///
+ /// The Alpha (transparency) component in the range from 0..1.
+ /// The Hue component in the range from 0..360.
+ /// Note that 360 is equivalent to 0 and will be adjusted automatically.
+ /// The Saturation component in the range from 0..1.
+ /// The Lightness component in the range from 0..1.
+ /// Whether to clamp component values to their required ranges.
+ internal HslColor(
+ double alpha,
+ double hue,
+ double saturation,
+ double lightness,
+ bool clampValues)
+ {
+ if (clampValues)
+ {
+ A = MathUtilities.Clamp(alpha, 0.0, 1.0);
+ H = MathUtilities.Clamp(hue, 0.0, 360.0);
+ S = MathUtilities.Clamp(saturation, 0.0, 1.0);
+ L = MathUtilities.Clamp(lightness, 0.0, 1.0);
+
+ // See comments in constructor above
+ H = (H == 360.0 ? 0 : H);
+ }
+ else
+ {
+ A = alpha;
+ H = hue;
+ S = saturation;
+ L = lightness;
+ }
+ }
+
+ ///
+ /// Initializes a new instance of the struct.
+ ///
+ /// The RGB color to convert to HSL.
+ public HslColor(Color color)
+ {
+ var hsl = Color.ToHsl(color);
+
+ A = hsl.A;
+ H = hsl.H;
+ S = hsl.S;
+ L = hsl.L;
+ }
+
+ ///
+ /// Gets the Alpha (transparency) component in the range from 0..1.
+ ///
+ public double A { get; }
+
+ ///
+ /// Gets the Hue component in the range from 0..360.
+ /// Note that 360 is equivalent to 0 and will be adjusted automatically.
+ ///
+ public double H { get; }
+
+ ///
+ /// Gets the Saturation component in the range from 0..1.
+ ///
+ public double S { get; }
+
+ ///
+ /// Gets the Lightness component in the range from 0..1.
+ ///
+ public double L { get; }
+
+ ///
+ public bool Equals(HslColor other)
+ {
+ return other.A == A &&
+ other.H == H &&
+ other.S == S &&
+ other.L == L;
+ }
+
+ ///
+ public override bool Equals(object? obj)
+ {
+ if (obj is HslColor hslColor)
+ {
+ return Equals(hslColor);
+ }
+ else
+ {
+ return false;
+ }
+ }
+
+ ///
+ /// Gets a hashcode for this object.
+ /// Hashcode is not guaranteed to be unique.
+ ///
+ /// The hashcode for this object.
+ public override int GetHashCode()
+ {
+ // Same algorithm as Color
+ // This is used instead of HashCode.Combine() due to .NET Standard 2.0 requirements
+ unchecked
+ {
+ int hashCode = A.GetHashCode();
+ hashCode = (hashCode * 397) ^ H.GetHashCode();
+ hashCode = (hashCode * 397) ^ S.GetHashCode();
+ hashCode = (hashCode * 397) ^ L.GetHashCode();
+ return hashCode;
+ }
+ }
+
+ ///
+ /// Returns the RGB color model equivalent of this HSL color.
+ ///
+ /// The RGB equivalent color.
+ public Color ToRgb()
+ {
+ // Use the by-component conversion method directly for performance
+ return HslColor.ToRgb(H, S, L, A);
+ }
+
+ ///
+ public override string ToString()
+ {
+ var sb = new StringBuilder();
+
+ // Use a format similar to CSS. However:
+ // - To ensure precision is never lost, allow decimal places.
+ // This is especially important for round-trip serialization.
+ // - To maintain numerical consistency, do not use percent.
+ //
+ // Example:
+ //
+ // hsla(hue, saturation, lightness, alpha)
+ // hsla(230, 1.0, 0.5, 1.0)
+ //
+
+ sb.Append("hsva(");
+ sb.Append(H.ToString(CultureInfo.InvariantCulture));
+ sb.Append(", ");
+ sb.Append(S.ToString(CultureInfo.InvariantCulture));
+ sb.Append(", ");
+ sb.Append(L.ToString(CultureInfo.InvariantCulture));
+ sb.Append(", ");
+ sb.Append(A.ToString(CultureInfo.InvariantCulture));
+ sb.Append(')');
+
+ return sb.ToString();
+ }
+
+ ///
+ /// Parses an HSL color string.
+ ///
+ /// The HSL color string to parse.
+ /// The parsed .
+ public static HslColor Parse(string s)
+ {
+ if (s is null)
+ {
+ throw new ArgumentNullException(nameof(s));
+ }
+
+ if (TryParse(s, out HslColor hslColor))
+ {
+ return hslColor;
+ }
+
+ throw new FormatException($"Invalid HSL color string: '{s}'.");
+ }
+
+ ///
+ /// Parses an HSL color string.
+ ///
+ /// The HSL color string to parse.
+ /// The parsed .
+ /// True if parsing was successful; otherwise, false.
+ public static bool TryParse(string s, out HslColor hslColor)
+ {
+ hslColor = default;
+
+ if (s is null)
+ {
+ return false;
+ }
+
+ string workingString = s.Trim();
+
+ if (workingString.Length == 0 ||
+ workingString.IndexOf(",", StringComparison.Ordinal) < 0)
+ {
+ return false;
+ }
+
+ if (workingString.Length > 6 &&
+ workingString.StartsWith("hsla(", StringComparison.OrdinalIgnoreCase) &&
+ workingString.EndsWith(")", StringComparison.Ordinal))
+ {
+ workingString = workingString.Substring(5, workingString.Length - 6);
+ }
+
+ if (workingString.Length > 5 &&
+ workingString.StartsWith("hsl(", StringComparison.OrdinalIgnoreCase) &&
+ workingString.EndsWith(")", StringComparison.Ordinal))
+ {
+ workingString = workingString.Substring(4, workingString.Length - 5);
+ }
+
+ string[] components = workingString.Split(',');
+
+ if (components.Length == 3) // HSL
+ {
+ if (double.TryParse(components[0], NumberStyles.Number, CultureInfo.InvariantCulture, out double hue) &&
+ TryInternalParse(components[1], out double saturation) &&
+ TryInternalParse(components[2], out double lightness))
+ {
+ hslColor = new HslColor(1.0, hue, saturation, lightness);
+ return true;
+ }
+ }
+ else if (components.Length == 4) // HSLA
+ {
+ if (double.TryParse(components[0], NumberStyles.Number, CultureInfo.InvariantCulture, out double hue) &&
+ TryInternalParse(components[1], out double saturation) &&
+ TryInternalParse(components[2], out double lightness) &&
+ TryInternalParse(components[3], out double alpha))
+ {
+ hslColor = new HslColor(alpha, hue, saturation, lightness);
+ return true;
+ }
+ }
+
+ // Local function to specially parse a double value with an optional percentage sign
+ bool TryInternalParse(string inString, out double outDouble)
+ {
+ // The percent sign, if it exists, must be at the end of the number
+ int percentIndex = inString.IndexOf("%", StringComparison.Ordinal);
+
+ if (percentIndex >= 0)
+ {
+ var result = double.TryParse(
+ inString.Substring(0, percentIndex),
+ NumberStyles.Number,
+ CultureInfo.InvariantCulture,
+ out double percentage);
+
+ outDouble = percentage / 100.0;
+ return result;
+ }
+ else
+ {
+ return double.TryParse(
+ inString,
+ NumberStyles.Number,
+ CultureInfo.InvariantCulture,
+ out outDouble);
+ }
+ }
+
+ return false;
+ }
+
+ ///
+ /// Creates a new from individual color component values.
+ ///
+ ///
+ /// This exists for symmetry with the struct; however, the
+ /// appropriate constructor should commonly be used instead.
+ ///
+ /// The Alpha (transparency) component in the range from 0..1.
+ /// The Hue component in the range from 0..360.
+ /// The Saturation component in the range from 0..1.
+ /// The Lightness component in the range from 0..1.
+ /// A new built from the individual color component values.
+ public static HslColor FromAhsl(double a, double h, double s, double l)
+ {
+ return new HslColor(a, h, s, l);
+ }
+
+ ///
+ /// Creates a new from individual color component values.
+ ///
+ ///
+ /// This exists for symmetry with the struct; however, the
+ /// appropriate constructor should commonly be used instead.
+ ///
+ /// The Hue component in the range from 0..360.
+ /// The Saturation component in the range from 0..1.
+ /// The Lightness component in the range from 0..1.
+ /// A new built from the individual color component values.
+ public static HslColor FromHsl(double h, double s, double l)
+ {
+ return new HslColor(1.0, h, s, l);
+ }
+
+ ///
+ /// Converts the given HSL color to its RGB color equivalent.
+ ///
+ /// The color in the HSL color model.
+ /// A new RGB equivalent to the given HSLA values.
+ public static Color ToRgb(HslColor hslColor)
+ {
+ return HslColor.ToRgb(hslColor.H, hslColor.S, hslColor.L, hslColor.A);
+ }
+
+ ///
+ /// Converts the given HSLA color component values to their RGB color equivalent.
+ ///
+ /// The Hue component in the HSL color model in the range from 0..360.
+ /// The Saturation component in the HSL color model in the range from 0..1.
+ /// The Lightness component in the HSL color model in the range from 0..1.
+ /// The Alpha component in the range from 0..1.
+ /// A new RGB equivalent to the given HSLA values.
+ public static Color ToRgb(
+ double hue,
+ double saturation,
+ double lightness,
+ double alpha = 1.0)
+ {
+ // Note: Conversion code is originally based on ColorHelper in the Windows Community Toolkit (licensed MIT)
+ // https://github.com/CommunityToolkit/WindowsCommunityToolkit/blob/main/Microsoft.Toolkit.Uwp/Helpers/ColorHelper.cs
+ // It has been modified to ensure input ranges and not throw exceptions.
+
+ // We want the hue to be between 0 and 359,
+ // so we first ensure that that's the case.
+ while (hue >= 360.0)
+ {
+ hue -= 360.0;
+ }
+
+ while (hue < 0.0)
+ {
+ hue += 360.0;
+ }
+
+ // We similarly clamp saturation, lightness and alpha between 0 and 1.
+ saturation = saturation < 0.0 ? 0.0 : saturation;
+ saturation = saturation > 1.0 ? 1.0 : saturation;
+
+ lightness = lightness < 0.0 ? 0.0 : lightness;
+ lightness = lightness > 1.0 ? 1.0 : lightness;
+
+ alpha = alpha < 0.0 ? 0.0 : alpha;
+ alpha = alpha > 1.0 ? 1.0 : alpha;
+
+ double chroma = (1 - Math.Abs((2 * lightness) - 1)) * saturation;
+ double h1 = hue / 60;
+ double x = chroma * (1 - Math.Abs((h1 % 2) - 1));
+ double m = lightness - (0.5 * chroma);
+ double r1, g1, b1;
+
+ if (h1 < 1)
+ {
+ r1 = chroma;
+ g1 = x;
+ b1 = 0;
+ }
+ else if (h1 < 2)
+ {
+ r1 = x;
+ g1 = chroma;
+ b1 = 0;
+ }
+ else if (h1 < 3)
+ {
+ r1 = 0;
+ g1 = chroma;
+ b1 = x;
+ }
+ else if (h1 < 4)
+ {
+ r1 = 0;
+ g1 = x;
+ b1 = chroma;
+ }
+ else if (h1 < 5)
+ {
+ r1 = x;
+ g1 = 0;
+ b1 = chroma;
+ }
+ else
+ {
+ r1 = chroma;
+ g1 = 0;
+ b1 = x;
+ }
+
+ return Color.FromArgb(
+ (byte)Math.Round(255 * alpha),
+ (byte)Math.Round(255 * (r1 + m)),
+ (byte)Math.Round(255 * (g1 + m)),
+ (byte)Math.Round(255 * (b1 + m)));
+ }
+
+ ///
+ /// Indicates whether the values of two specified objects are equal.
+ ///
+ /// The first object to compare.
+ /// The second object to compare.
+ /// True if left and right are equal; otherwise, false.
+ public static bool operator ==(HslColor left, HslColor right)
+ {
+ return left.Equals(right);
+ }
+
+ ///
+ /// Indicates whether the values of two specified objects are not equal.
+ ///
+ /// The first object to compare.
+ /// The second object to compare.
+ /// True if left and right are not equal; otherwise, false.
+ public static bool operator !=(HslColor left, HslColor right)
+ {
+ return !(left == right);
+ }
+
+ ///
+ /// Explicit conversion from an to a .
+ ///
+ /// The to convert.
+ public static explicit operator Color(HslColor hslColor)
+ {
+ return hslColor.ToRgb();
+ }
+ }
+}
diff --git a/src/Avalonia.Visuals/Media/HsvColor.cs b/src/Avalonia.Visuals/Media/HsvColor.cs
index 8b2f10d088..164aeb1df1 100644
--- a/src/Avalonia.Visuals/Media/HsvColor.cs
+++ b/src/Avalonia.Visuals/Media/HsvColor.cs
@@ -1,9 +1,11 @@
-// Color conversion portions of this source file are adapted from the WinUI project.
-// (https://github.com/microsoft/microsoft-ui-xaml)
-//
+// Color conversion portions of this source file are adapted from the WinUI project.
+// (https://github.com/microsoft/microsoft-ui-xaml)
+//
// Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation.
using System;
+using System.Globalization;
+using System.Text;
using Avalonia.Utilities;
namespace Avalonia.Media
@@ -19,10 +21,11 @@ namespace Avalonia.Media
///
/// Initializes a new instance of the struct.
///
- /// The Alpha (transparency) channel value in the range from 0..1.
- /// The Hue channel value in the range from 0..360.
- /// The Saturation channel value in the range from 0..1.
- /// The Value channel value in the range from 0..1.
+ /// The Alpha (transparency) component in the range from 0..1.
+ /// The Hue component in the range from 0..360.
+ /// Note that 360 is equivalent to 0 and will be adjusted automatically.
+ /// The Saturation component in the range from 0..1.
+ /// The Value component in the range from 0..1.
public HsvColor(
double alpha,
double hue,
@@ -33,6 +36,12 @@ namespace Avalonia.Media
H = MathUtilities.Clamp(hue, 0.0, 360.0);
S = MathUtilities.Clamp(saturation, 0.0, 1.0);
V = MathUtilities.Clamp(value, 0.0, 1.0);
+
+ // The maximum value of Hue is technically 360 minus epsilon (just below 360).
+ // This is because, in a color circle, 360 degrees is equivalent to 0 degrees.
+ // However, that is too tricky to work with in code and isn't as intuitive.
+ // Therefore, since 360 == 0, just wrap 360 if needed back to 0.
+ H = (H == 360.0 ? 0 : H);
}
///
@@ -40,13 +49,14 @@ namespace Avalonia.Media
///
///
/// This constructor exists only for internal use where performance is critical.
- /// Whether or not the channel values are in the correct ranges must be known.
+ /// Whether or not the component values are in the correct ranges must be known.
///
- /// The Alpha (transparency) channel value in the range from 0..1.
- /// The Hue channel value in the range from 0..360.
- /// The Saturation channel value in the range from 0..1.
- /// The Value channel value in the range from 0..1.
- /// Whether to clamp channel values to their required ranges.
+ /// The Alpha (transparency) component in the range from 0..1.
+ /// The Hue component in the range from 0..360.
+ /// Note that 360 is equivalent to 0 and will be adjusted automatically.
+ /// The Saturation component in the range from 0..1.
+ /// The Value component in the range from 0..1.
+ /// Whether to clamp component values to their required ranges.
internal HsvColor(
double alpha,
double hue,
@@ -60,6 +70,9 @@ namespace Avalonia.Media
H = MathUtilities.Clamp(hue, 0.0, 360.0);
S = MathUtilities.Clamp(saturation, 0.0, 1.0);
V = MathUtilities.Clamp(value, 0.0, 1.0);
+
+ // See comments in constructor above
+ H = (H == 360.0 ? 0 : H);
}
else
{
@@ -76,7 +89,7 @@ namespace Avalonia.Media
/// The RGB color to convert to HSV.
public HsvColor(Color color)
{
- var hsv = HsvColor.FromRgb(color);
+ var hsv = Color.ToHsv(color);
A = hsv.A;
H = hsv.H;
@@ -85,22 +98,23 @@ namespace Avalonia.Media
}
///
- /// Gets the Alpha (transparency) channel value in the range from 0..1.
+ /// Gets the Alpha (transparency) component in the range from 0..1.
///
public double A { get; }
///
- /// Gets the Hue channel value in the range from 0..360.
+ /// Gets the Hue component in the range from 0..360.
+ /// Note that 360 is equivalent to 0 and will be adjusted automatically.
///
public double H { get; }
///
- /// Gets the Saturation channel value in the range from 0..1.
+ /// Gets the Saturation component in the range from 0..1.
///
public double S { get; }
///
- /// Gets the Value channel value in the range from 0..1.
+ /// Gets the Value component in the range from 0..1.
///
public double V { get; }
@@ -151,29 +165,185 @@ namespace Avalonia.Media
/// The RGB equivalent color.
public Color ToRgb()
{
- // Use the by-channel conversion method directly for performance
+ // Use the by-component conversion method directly for performance
return HsvColor.ToRgb(H, S, V, A);
}
+ ///
+ public override string ToString()
+ {
+ var sb = new StringBuilder();
+
+ // Use a format similar to CSS. However:
+ // - To ensure precision is never lost, allow decimal places.
+ // This is especially important for round-trip serialization.
+ // - To maintain numerical consistency, do not use percent.
+ //
+ // Example:
+ //
+ // hsva(hue, saturation, value, alpha)
+ // hsva(230, 1.0, 0.5, 1.0)
+ //
+
+ sb.Append("hsva(");
+ sb.Append(H.ToString(CultureInfo.InvariantCulture));
+ sb.Append(", ");
+ sb.Append(S.ToString(CultureInfo.InvariantCulture));
+ sb.Append(", ");
+ sb.Append(V.ToString(CultureInfo.InvariantCulture));
+ sb.Append(", ");
+ sb.Append(A.ToString(CultureInfo.InvariantCulture));
+ sb.Append(')');
+
+ return sb.ToString();
+ }
+
+ ///
+ /// Parses an HSV color string.
+ ///
+ /// The HSV color string to parse.
+ /// The parsed .
+ public static HsvColor Parse(string s)
+ {
+ if (s is null)
+ {
+ throw new ArgumentNullException(nameof(s));
+ }
+
+ if (TryParse(s, out HsvColor hsvColor))
+ {
+ return hsvColor;
+ }
+
+ throw new FormatException($"Invalid HSV color string: '{s}'.");
+ }
+
+ ///
+ /// Parses an HSV color string.
+ ///
+ /// The HSV color string to parse.
+ /// The parsed .
+ /// True if parsing was successful; otherwise, false.
+ public static bool TryParse(string s, out HsvColor hsvColor)
+ {
+ hsvColor = default;
+
+ if (s is null)
+ {
+ return false;
+ }
+
+ string workingString = s.Trim();
+
+ if (workingString.Length == 0 ||
+ workingString.IndexOf(",", StringComparison.Ordinal) < 0)
+ {
+ return false;
+ }
+
+ if (workingString.Length > 6 &&
+ workingString.StartsWith("hsva(", StringComparison.OrdinalIgnoreCase) &&
+ workingString.EndsWith(")", StringComparison.Ordinal))
+ {
+ workingString = workingString.Substring(5, workingString.Length - 6);
+ }
+
+ if (workingString.Length > 5 &&
+ workingString.StartsWith("hsv(", StringComparison.OrdinalIgnoreCase) &&
+ workingString.EndsWith(")", StringComparison.Ordinal))
+ {
+ workingString = workingString.Substring(4, workingString.Length - 5);
+ }
+
+ string[] components = workingString.Split(',');
+
+ if (components.Length == 3) // HSV
+ {
+ if (double.TryParse(components[0], NumberStyles.Number, CultureInfo.InvariantCulture, out double hue) &&
+ TryInternalParse(components[1], out double saturation) &&
+ TryInternalParse(components[2], out double value))
+ {
+ hsvColor = new HsvColor(1.0, hue, saturation, value);
+ return true;
+ }
+ }
+ else if (components.Length == 4) // HSVA
+ {
+ if (double.TryParse(components[0], NumberStyles.Number, CultureInfo.InvariantCulture, out double hue) &&
+ TryInternalParse(components[1], out double saturation) &&
+ TryInternalParse(components[2], out double value) &&
+ TryInternalParse(components[3], out double alpha))
+ {
+ hsvColor = new HsvColor(alpha, hue, saturation, value);
+ return true;
+ }
+ }
+
+ // Local function to specially parse a double value with an optional percentage sign
+ bool TryInternalParse(string inString, out double outDouble)
+ {
+ // The percent sign, if it exists, must be at the end of the number
+ int percentIndex = inString.IndexOf("%", StringComparison.Ordinal);
+
+ if (percentIndex >= 0)
+ {
+ var result = double.TryParse(
+ inString.Substring(0, percentIndex),
+ NumberStyles.Number,
+ CultureInfo.InvariantCulture,
+ out double percentage);
+
+ outDouble = percentage / 100.0;
+ return result;
+ }
+ else
+ {
+ return double.TryParse(
+ inString,
+ NumberStyles.Number,
+ CultureInfo.InvariantCulture,
+ out outDouble);
+ }
+ }
+
+ return false;
+ }
+
///
- /// Creates a new from individual color channel values.
+ /// Creates a new from individual color component values.
///
///
/// This exists for symmetry with the struct; however, the
/// appropriate constructor should commonly be used instead.
///
- /// The Alpha (transparency) channel value in the range from 0..1.
- /// The Hue channel value in the range from 0..360.
- /// The Saturation channel value in the range from 0..1.
- /// The Value channel value in the range from 0..1.
- /// A new built from the individual color channel values.
+ /// The Alpha (transparency) component in the range from 0..1.
+ /// The Hue component in the range from 0..360.
+ /// The Saturation component in the range from 0..1.
+ /// The Value component in the range from 0..1.
+ /// A new built from the individual color component values.
public static HsvColor FromAhsv(double a, double h, double s, double v)
{
return new HsvColor(a, h, s, v);
}
///
- /// Converts the given HSV color to it's RGB color equivalent.
+ /// Creates a new from individual color component values.
+ ///
+ ///
+ /// This exists for symmetry with the struct; however, the
+ /// appropriate constructor should commonly be used instead.
+ ///
+ /// The Hue component in the range from 0..360.
+ /// The Saturation component in the range from 0..1.
+ /// The Value component in the range from 0..1.
+ /// A new built from the individual color component values.
+ public static HsvColor FromHsv(double h, double s, double v)
+ {
+ return new HsvColor(1.0, h, s, v);
+ }
+
+ ///
+ /// Converts the given HSV color to its RGB color equivalent.
///
/// The color in the HSV color model.
/// A new RGB equivalent to the given HSVA values.
@@ -183,12 +353,12 @@ namespace Avalonia.Media
}
///
- /// Converts the given HSVA color channel values to it's RGB color equivalent.
+ /// Converts the given HSVA color component values to their RGB color equivalent.
///
- /// The hue channel value in the HSV color model in the range from 0..360.
- /// The saturation channel value in the HSV color model in the range from 0..1.
- /// The value channel value in the HSV color model in the range from 0..1.
- /// The alpha channel value in the range from 0..1.
+ /// The Hue component in the HSV color model in the range from 0..360.
+ /// The Saturation component in the HSV color model in the range from 0..1.
+ /// The Value component in the HSV color model in the range from 0..1.
+ /// The Alpha component in the range from 0..1.
/// A new RGB equivalent to the given HSVA values.
public static Color ToRgb(
double hue,
@@ -199,7 +369,7 @@ namespace Avalonia.Media
// Note: Conversion code is originally based on the C++ in WinUI (licensed MIT)
// https://github.com/microsoft/microsoft-ui-xaml/blob/main/dev/Common/ColorConversion.cpp
// This was used because it is the best documented and likely most optimized for performance
- // Alpha channel support was added
+ // Alpha support was added
// We want the hue to be between 0 and 359,
// so we first ensure that that's the case.
@@ -320,130 +490,6 @@ namespace Avalonia.Media
(byte)Math.Round(b * 255));
}
- ///
- /// Converts the given RGB color to it's HSV color equivalent.
- ///
- /// The color in the RGB color model.
- /// A new equivalent to the given RGBA values.
- public static HsvColor FromRgb(Color color)
- {
- return HsvColor.FromRgb(color.R, color.G, color.B, color.A);
- }
-
- ///
- /// Converts the given RGBA color channel values to it's HSV color equivalent.
- ///
- /// The red channel value in the RGB color model.
- /// The green channel value in the RGB color model.
- /// The blue channel value in the RGB color model.
- /// The alpha channel value.
- /// A new equivalent to the given RGBA values.
- public static HsvColor FromRgb(
- byte red,
- byte green,
- byte blue,
- byte alpha = 0xFF)
- {
- // Note: Conversion code is originally based on the C++ in WinUI (licensed MIT)
- // https://github.com/microsoft/microsoft-ui-xaml/blob/main/dev/Common/ColorConversion.cpp
- // This was used because it is the best documented and likely most optimized for performance
- // Alpha channel support was added
-
- // Normalize RGBA channel values into the 0..1 range used by this algorithm
- double r = red / 255.0;
- double g = green / 255.0;
- double b = blue / 255.0;
- double a = alpha / 255.0;
-
- double hue;
- double saturation;
- double value;
-
- double max = r >= g ? (r >= b ? r : b) : (g >= b ? g : b);
- double min = r <= g ? (r <= b ? r : b) : (g <= b ? g : b);
-
- // The value, a number between 0 and 1, is the largest of R, G, and B (divided by 255).
- // Conceptually speaking, it represents how much color is present.
- // If at least one of R, G, B is 255, then there exists as much color as there can be.
- // If RGB = (0, 0, 0), then there exists no color at all - a value of zero corresponds
- // to black (i.e., the absence of any color).
- value = max;
-
- // The "chroma" of the color is a value directly proportional to the extent to which
- // the color diverges from greyscale. If, for example, we have RGB = (255, 255, 0),
- // then the chroma is maximized - this is a pure yellow, no gray of any kind.
- // On the other hand, if we have RGB = (128, 128, 128), then the chroma being zero
- // implies that this color is pure greyscale, with no actual hue to be found.
- var chroma = max - min;
-
- // If the chrome is zero, then hue is technically undefined - a greyscale color
- // has no hue. For the sake of convenience, we'll just set hue to zero, since
- // it will be unused in this circumstance. Since the color is purely gray,
- // saturation is also equal to zero - you can think of saturation as basically
- // a measure of hue intensity, such that no hue at all corresponds to a
- // nonexistent intensity.
- if (chroma == 0)
- {
- hue = 0.0;
- saturation = 0.0;
- }
- else
- {
- // In this block, hue is properly defined, so we'll extract both hue
- // and saturation information from the RGB color.
-
- // Hue can be thought of as a cyclical thing, between 0 degrees and 360 degrees.
- // A hue of 0 degrees is red; 120 degrees is green; 240 degrees is blue; and 360 is back to red.
- // Every other hue is somewhere between either red and green, green and blue, and blue and red,
- // so every other hue can be thought of as an angle on this color wheel.
- // These if/else statements determines where on this color wheel our color lies.
- if (r == max)
- {
- // If the red channel is the most pronounced channel, then we exist
- // somewhere between (-60, 60) on the color wheel - i.e., the section around 0 degrees
- // where red dominates. We figure out where in that section we are exactly
- // by considering whether the green or the blue channel is greater - by subtracting green from blue,
- // then if green is greater, we'll nudge ourselves closer to 60, whereas if blue is greater, then
- // we'll nudge ourselves closer to -60. We then divide by chroma (which will actually make the result larger,
- // since chroma is a value between 0 and 1) to normalize the value to ensure that we get the right hue
- // even if we're very close to greyscale.
- hue = 60 * (g - b) / chroma;
- }
- else if (g == max)
- {
- // We do the exact same for the case where the green channel is the most pronounced channel,
- // only this time we want to see if we should tilt towards the blue direction or the red direction.
- // We add 120 to center our value in the green third of the color wheel.
- hue = 120 + (60 * (b - r) / chroma);
- }
- else // blue == max
- {
- // And we also do the exact same for the case where the blue channel is the most pronounced channel,
- // only this time we want to see if we should tilt towards the red direction or the green direction.
- // We add 240 to center our value in the blue third of the color wheel.
- hue = 240 + (60 * (r - g) / chroma);
- }
-
- // Since we want to work within the range [0, 360), we'll add 360 to any value less than zero -
- // this will bump red values from within -60 to -1 to 300 to 359. The hue is the same at both values.
- if (hue < 0.0)
- {
- hue += 360.0;
- }
-
- // The saturation, our final HSV axis, can be thought of as a value between 0 and 1 indicating how intense our color is.
- // To find it, we divide the chroma - the distance between the minimum and the maximum RGB channels - by the maximum channel (i.e., the value).
- // This effectively normalizes the chroma - if the maximum is 0.5 and the minimum is 0, the saturation will be (0.5 - 0) / 0.5 = 1,
- // meaning that although this color is not as bright as it can be, the dark color is as intense as it possibly could be.
- // If, on the other hand, the maximum is 0.5 and the minimum is 0.25, then the saturation will be (0.5 - 0.25) / 0.5 = 0.5,
- // meaning that this color is partially washed out.
- // A saturation value of 0 corresponds to a greyscale color, one in which the color is *completely* washed out and there is no actual hue.
- saturation = chroma / value;
- }
-
- return new HsvColor(a, hue, saturation, value, false);
- }
-
///
/// Indicates whether the values of two specified objects are equal.
///
diff --git a/src/Avalonia.Visuals/Media/StreamGeometryContext.cs b/src/Avalonia.Visuals/Media/StreamGeometryContext.cs
index 88aba8365e..ed6065eae4 100644
--- a/src/Avalonia.Visuals/Media/StreamGeometryContext.cs
+++ b/src/Avalonia.Visuals/Media/StreamGeometryContext.cs
@@ -41,7 +41,7 @@ namespace Avalonia.Media
///
/// The destination point.
/// The radii of an oval whose perimeter is used to draw the angle.
- /// The rotation angle of the oval that specifies the curve.
+ /// The rotation angle (in radians) of the oval that specifies the curve.
/// true to draw the arc greater than 180 degrees; otherwise, false.
///
/// A value that indicates whether the arc is drawn in the Clockwise or Counterclockwise direction.
@@ -59,7 +59,7 @@ namespace Avalonia.Media
///
/// The destination point.
/// The radii of an oval whose perimeter is used to draw the angle.
- /// The rotation angle of the oval that specifies the curve.
+ /// The rotation angle (in radians) of the oval that specifies the curve.
/// true to draw the arc greater than 180 degrees; otherwise, false.
///
/// A value that indicates whether the arc is drawn in the Clockwise or Counterclockwise direction.
diff --git a/src/Avalonia.Visuals/Media/TextDecorationCollection.cs b/src/Avalonia.Visuals/Media/TextDecorationCollection.cs
index 2dced2252e..2d7bd17b20 100644
--- a/src/Avalonia.Visuals/Media/TextDecorationCollection.cs
+++ b/src/Avalonia.Visuals/Media/TextDecorationCollection.cs
@@ -10,6 +10,16 @@ namespace Avalonia.Media
///
public class TextDecorationCollection : AvaloniaList
{
+ public TextDecorationCollection()
+ {
+
+ }
+
+ public TextDecorationCollection(IEnumerable textDecorations) : base(textDecorations)
+ {
+
+ }
+
///
/// Parses a string.
///
diff --git a/src/Avalonia.Visuals/Media/TextFormatting/DrawableTextRun.cs b/src/Avalonia.Visuals/Media/TextFormatting/DrawableTextRun.cs
index 3757a4506a..e63ed421ba 100644
--- a/src/Avalonia.Visuals/Media/TextFormatting/DrawableTextRun.cs
+++ b/src/Avalonia.Visuals/Media/TextFormatting/DrawableTextRun.cs
@@ -9,6 +9,11 @@
/// Gets the size.
///
public abstract Size Size { get; }
+
+ ///
+ /// Run baseline in ratio relative to run height
+ ///
+ public abstract double Baseline { get; }
///
/// Draws the at the given origin.
diff --git a/src/Avalonia.Visuals/Media/TextFormatting/FormattedTextSource.cs b/src/Avalonia.Visuals/Media/TextFormatting/FormattedTextSource.cs
index 1b0feaa718..97df87d3d9 100644
--- a/src/Avalonia.Visuals/Media/TextFormatting/FormattedTextSource.cs
+++ b/src/Avalonia.Visuals/Media/TextFormatting/FormattedTextSource.cs
@@ -1,6 +1,5 @@
using System;
using System.Collections.Generic;
-using Avalonia.Media.TextFormatting.Unicode;
using Avalonia.Utilities;
namespace Avalonia.Media.TextFormatting
@@ -30,10 +29,10 @@ namespace Avalonia.Media.TextFormatting
if (runText.IsEmpty)
{
- return new TextEndOfParagraph();
+ return null;
}
- var textStyleRun = CreateTextStyleRun(runText, _defaultProperties, _textModifier);
+ var textStyleRun = CreateTextStyleRun(runText, textSourceIndex, _defaultProperties, _textModifier);
return new TextCharacters(runText.Take(textStyleRun.Length), textStyleRun.Value);
}
@@ -42,17 +41,18 @@ namespace Avalonia.Media.TextFormatting
/// Creates a span of text run properties that has modifier applied.
///
/// The text to create the properties for.
+ /// The first text source index.
/// The default text properties.
/// The text properties modifier.
///
/// The created text style run.
///
- private static ValueSpan CreateTextStyleRun(ReadOnlySlice text,
+ private static ValueSpan CreateTextStyleRun(ReadOnlySlice text, int firstTextSourceIndex,
TextRunProperties defaultProperties, IReadOnlyList>? textModifier)
{
if (textModifier == null || textModifier.Count == 0)
{
- return new ValueSpan(text.Start, text.Length, defaultProperties);
+ return new ValueSpan(firstTextSourceIndex, text.Length, defaultProperties);
}
var currentProperties = defaultProperties;
@@ -69,28 +69,28 @@ namespace Avalonia.Media.TextFormatting
var textRange = new TextRange(propertiesOverride.Start, propertiesOverride.Length);
- if (textRange.Start + textRange.Length <= text.Start)
+ if (textRange.Start + textRange.Length <= firstTextSourceIndex)
{
continue;
}
- if (textRange.Start > text.End)
+ if (textRange.Start > firstTextSourceIndex + text.Length)
{
length = text.Length;
break;
}
- if (textRange.Start > text.Start)
+ if (textRange.Start > firstTextSourceIndex)
{
if (propertiesOverride.Value != currentProperties)
{
- length = Math.Min(Math.Abs(textRange.Start - text.Start), text.Length);
+ length = Math.Min(Math.Abs(textRange.Start - firstTextSourceIndex), text.Length);
break;
}
}
- length += Math.Max(0, textRange.Start + textRange.Length - text.Start);
+ length = Math.Max(0, textRange.Start + textRange.Length - firstTextSourceIndex);
if (hasOverride)
{
@@ -116,12 +116,7 @@ namespace Avalonia.Media.TextFormatting
length = text.Length;
}
- if (length != text.Length)
- {
- text = text.Take(length);
- }
-
- return new ValueSpan(text.Start, length, currentProperties);
+ return new ValueSpan(firstTextSourceIndex, length, currentProperties);
}
}
}
diff --git a/src/Avalonia.Visuals/Media/TextFormatting/ShapedTextCharacters.cs b/src/Avalonia.Visuals/Media/TextFormatting/ShapedTextCharacters.cs
index fb85766003..53287a264d 100644
--- a/src/Avalonia.Visuals/Media/TextFormatting/ShapedTextCharacters.cs
+++ b/src/Avalonia.Visuals/Media/TextFormatting/ShapedTextCharacters.cs
@@ -38,6 +38,8 @@ namespace Avalonia.Media.TextFormatting
public FontMetrics FontMetrics { get; }
+ public override double Baseline => -FontMetrics.Ascent;
+
public override Size Size => GlyphRun.Size;
public GlyphRun GlyphRun
diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextBounds.cs b/src/Avalonia.Visuals/Media/TextFormatting/TextBounds.cs
new file mode 100644
index 0000000000..a0b51671f0
--- /dev/null
+++ b/src/Avalonia.Visuals/Media/TextFormatting/TextBounds.cs
@@ -0,0 +1,29 @@
+using System.Collections.Generic;
+
+namespace Avalonia.Media.TextFormatting
+{
+ ///
+ /// The bounding rectangle of a range of characters
+ ///
+ public sealed class TextBounds
+ {
+ ///
+ /// Constructing TextBounds object
+ ///
+ internal TextBounds(Rect bounds, FlowDirection flowDirection)
+ {
+ Rectangle = bounds;
+ FlowDirection = flowDirection;
+ }
+
+ ///
+ /// Bounds rectangle
+ ///
+ public Rect Rectangle { get; }
+
+ ///
+ /// Text flow direction inside the boundary rectangle
+ ///
+ public FlowDirection FlowDirection { get; }
+ }
+}
diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextCharacters.cs b/src/Avalonia.Visuals/Media/TextFormatting/TextCharacters.cs
index c4b2dfb3a5..ab72601c3e 100644
--- a/src/Avalonia.Visuals/Media/TextFormatting/TextCharacters.cs
+++ b/src/Avalonia.Visuals/Media/TextFormatting/TextCharacters.cs
@@ -38,7 +38,7 @@ namespace Avalonia.Media.TextFormatting
/// Gets a list of .
///
/// The shapeable text characters.
- internal IList GetShapeableCharacters(ReadOnlySlice runText, sbyte biDiLevel,
+ internal IReadOnlyList GetShapeableCharacters(ReadOnlySlice runText, sbyte biDiLevel,
ref TextRunProperties? previousProperties)
{
var shapeableCharacters = new List(2);
@@ -72,11 +72,11 @@ namespace Avalonia.Media.TextFormatting
var currentTypeface = defaultTypeface;
var previousTypeface = previousProperties?.Typeface;
- if (TryGetShapeableLength(text, currentTypeface, out var count, out var script))
+ if (TryGetShapeableLength(text, currentTypeface, null, out var count, out var script))
{
if (script == Script.Common && previousTypeface is not null)
{
- if(TryGetShapeableLength(text, previousTypeface.Value, out var fallbackCount, out _))
+ if(TryGetShapeableLength(text, previousTypeface.Value, defaultTypeface, out var fallbackCount, out _))
{
return new ShapeableTextCharacters(text.Take(fallbackCount),
defaultProperties.WithTypeface(previousTypeface.Value), biDiLevel);
@@ -89,7 +89,7 @@ namespace Avalonia.Media.TextFormatting
if (previousTypeface is not null)
{
- if(TryGetShapeableLength(text, previousTypeface.Value, out count, out _))
+ if(TryGetShapeableLength(text, previousTypeface.Value, defaultTypeface, out count, out _))
{
return new ShapeableTextCharacters(text.Take(count),
defaultProperties.WithTypeface(previousTypeface.Value), biDiLevel);
@@ -118,7 +118,7 @@ namespace Avalonia.Media.TextFormatting
defaultTypeface.Stretch, defaultTypeface.FontFamily, defaultProperties.CultureInfo,
out currentTypeface);
- if (matchFound && TryGetShapeableLength(text, currentTypeface, out count, out _))
+ if (matchFound && TryGetShapeableLength(text, currentTypeface, defaultTypeface, out count, out _))
{
//Fallback found
return new ShapeableTextCharacters(text.Take(count), defaultProperties.WithTypeface(currentTypeface),
@@ -152,14 +152,19 @@ namespace Avalonia.Media.TextFormatting
///
/// The text.
/// The typeface that is used to find matching characters.
+ ///
/// The shapeable length.
///
///
- protected static bool TryGetShapeableLength(ReadOnlySlice text, Typeface typeface, out int length,
+ protected static bool TryGetShapeableLength(
+ ReadOnlySlice text,
+ Typeface typeface,
+ Typeface? defaultTypeface,
+ out int length,
out Script script)
{
length = 0;
- script = Script.Unknown;
+ script = Script.Unknown;
if (text.Length == 0)
{
@@ -167,6 +172,7 @@ namespace Avalonia.Media.TextFormatting
}
var font = typeface.GlyphTypeface;
+ var defaultFont = defaultTypeface?.GlyphTypeface;
var enumerator = new GraphemeEnumerator(text);
@@ -176,6 +182,11 @@ namespace Avalonia.Media.TextFormatting
var currentScript = currentGrapheme.FirstCodepoint.Script;
+ if (currentScript != Script.Common && defaultFont != null && defaultFont.TryGetGlyph(currentGrapheme.FirstCodepoint, out _))
+ {
+ break;
+ }
+
//Stop at the first missing glyph
if (!currentGrapheme.FirstCodepoint.IsBreakChar && !font.TryGetGlyph(currentGrapheme.FirstCodepoint, out _))
{
diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextCollapsingProperties.cs b/src/Avalonia.Visuals/Media/TextFormatting/TextCollapsingProperties.cs
index a46f9537d0..f677617b14 100644
--- a/src/Avalonia.Visuals/Media/TextFormatting/TextCollapsingProperties.cs
+++ b/src/Avalonia.Visuals/Media/TextFormatting/TextCollapsingProperties.cs
@@ -21,6 +21,6 @@ namespace Avalonia.Media.TextFormatting
/// Collapses given text line.
///
/// Text line to collapse.
- public abstract IReadOnlyList? Collapse(TextLine textLine);
+ public abstract List? Collapse(TextLine textLine);
}
}
diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextEllipsisHelper.cs b/src/Avalonia.Visuals/Media/TextFormatting/TextEllipsisHelper.cs
index 2031c2ec99..5a2169630b 100644
--- a/src/Avalonia.Visuals/Media/TextFormatting/TextEllipsisHelper.cs
+++ b/src/Avalonia.Visuals/Media/TextFormatting/TextEllipsisHelper.cs
@@ -3,13 +3,11 @@ using Avalonia.Media.TextFormatting.Unicode;
namespace Avalonia.Media.TextFormatting
{
- internal class TextEllipsisHelper
+ internal static class TextEllipsisHelper
{
- public static List? Collapse(TextLine textLine, TextCollapsingProperties properties, bool isWordEllipsis)
+ public static List? Collapse(TextLine textLine, TextCollapsingProperties properties, bool isWordEllipsis)
{
- var shapedTextRuns = textLine.TextRuns as List;
-
- if (shapedTextRuns is null)
+ if (textLine.TextRuns is not List textRuns || textRuns.Count == 0)
{
return null;
}
@@ -17,74 +15,103 @@ namespace Avalonia.Media.TextFormatting
var runIndex = 0;
var currentWidth = 0.0;
var collapsedLength = 0;
- var textRange = textLine.TextRange;
var shapedSymbol = TextFormatterImpl.CreateSymbol(properties.Symbol, FlowDirection.LeftToRight);
if (properties.Width < shapedSymbol.GlyphRun.Size.Width)
{
- return new List(0);
+ //Not enough space to fit in the symbol
+ return new List(0);
}
var availableWidth = properties.Width - shapedSymbol.Size.Width;
- while (runIndex < shapedTextRuns.Count)
+ while (runIndex < textRuns.Count)
{
- var currentRun = shapedTextRuns[runIndex];
-
- currentWidth += currentRun.Size.Width;
+ var currentRun = textRuns[runIndex];
- if (currentWidth > availableWidth)
+ switch (currentRun)
{
- if (currentRun.TryMeasureCharacters(availableWidth, out var measuredLength))
+ case ShapedTextCharacters shapedRun:
{
- if (isWordEllipsis && measuredLength < textRange.End)
+ currentWidth += shapedRun.Size.Width;
+
+ if (currentWidth > availableWidth)
{
- var currentBreakPosition = 0;
+ if (shapedRun.TryMeasureCharacters(availableWidth, out var measuredLength))
+ {
+ if (isWordEllipsis && measuredLength < textLine.Length)
+ {
+ var currentBreakPosition = 0;
- var lineBreaker = new LineBreakEnumerator(currentRun.Text);
+ var lineBreaker = new LineBreakEnumerator(currentRun.Text);
- while (currentBreakPosition < measuredLength && lineBreaker.MoveNext())
- {
- var nextBreakPosition = lineBreaker.Current.PositionMeasure;
+ while (currentBreakPosition < measuredLength && lineBreaker.MoveNext())
+ {
+ var nextBreakPosition = lineBreaker.Current.PositionMeasure;
- if (nextBreakPosition == 0)
- {
- break;
- }
+ if (nextBreakPosition == 0)
+ {
+ break;
+ }
- if (nextBreakPosition >= measuredLength)
- {
- break;
+ if (nextBreakPosition >= measuredLength)
+ {
+ break;
+ }
+
+ currentBreakPosition = nextBreakPosition;
+ }
+
+ measuredLength = currentBreakPosition;
}
+ }
+
+ collapsedLength += measuredLength;
- currentBreakPosition = nextBreakPosition;
+ var collapsedRuns = new List(textRuns.Count);
+
+ if (collapsedLength > 0)
+ {
+ var splitResult = TextFormatterImpl.SplitDrawableRuns(textRuns, collapsedLength);
+
+ collapsedRuns.AddRange(splitResult.First);
}
- measuredLength = currentBreakPosition;
+ collapsedRuns.Add(shapedSymbol);
+
+ return collapsedRuns;
}
- }
- collapsedLength += measuredLength;
+ availableWidth -= currentRun.Size.Width;
- var shapedTextCharacters = new List(shapedTextRuns.Count);
+
+ break;
+ }
- if (collapsedLength > 0)
+ case { } drawableRun:
{
- var splitResult = TextFormatterImpl.SplitShapedRuns(shapedTextRuns, collapsedLength);
+ //The whole run needs to fit into available space
+ if (currentWidth + drawableRun.Size.Width > availableWidth)
+ {
+ var collapsedRuns = new List(textRuns.Count);
- shapedTextCharacters.AddRange(splitResult.First);
+ if (collapsedLength > 0)
+ {
+ var splitResult = TextFormatterImpl.SplitDrawableRuns(textRuns, collapsedLength);
- TextLineImpl.SortRuns(shapedTextCharacters);
- }
+ collapsedRuns.AddRange(splitResult.First);
+ }
- shapedTextCharacters.Add(shapedSymbol);
+ collapsedRuns.Add(shapedSymbol);
- return shapedTextCharacters;
+ return collapsedRuns;
+ }
+
+ break;
+ }
}
- availableWidth -= currentRun.Size.Width;
-
- collapsedLength += currentRun.GlyphRun.Characters.Length;
+ collapsedLength += currentRun.TextSourceLength;
runIndex++;
}
diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextFormatterImpl.cs b/src/Avalonia.Visuals/Media/TextFormatting/TextFormatterImpl.cs
index 13ed850715..7c60f73b8d 100644
--- a/src/Avalonia.Visuals/Media/TextFormatting/TextFormatterImpl.cs
+++ b/src/Avalonia.Visuals/Media/TextFormatting/TextFormatterImpl.cs
@@ -1,8 +1,6 @@
using System;
using System.Collections.Generic;
-using System.Globalization;
using System.Linq;
-using System.Runtime.InteropServices;
using Avalonia.Media.TextFormatting.Unicode;
using Avalonia.Utilities;
@@ -17,22 +15,22 @@ namespace Avalonia.Media.TextFormatting
var textWrapping = paragraphProperties.TextWrapping;
FlowDirection flowDirection;
TextLineBreak? nextLineBreak = null;
- List shapedRuns;
+ List drawableTextRuns;
var textRuns = FetchTextRuns(textSource, firstTextSourceIndex,
- out var textEndOfLine, out var textRange);
+ out var textEndOfLine, out var textSourceLength);
- if (previousLineBreak?.RemainingCharacters != null)
+ if (previousLineBreak?.RemainingRuns != null)
{
flowDirection = previousLineBreak.FlowDirection;
- shapedRuns = previousLineBreak.RemainingCharacters.ToList();
+ drawableTextRuns = previousLineBreak.RemainingRuns.ToList();
nextLineBreak = previousLineBreak;
}
else
{
- shapedRuns = ShapeTextRuns(textRuns, paragraphProperties.FlowDirection,out flowDirection);
+ drawableTextRuns = ShapeTextRuns(textRuns, paragraphProperties, out flowDirection);
- if(nextLineBreak == null && textEndOfLine != null)
+ if (nextLineBreak == null && textEndOfLine != null)
{
nextLineBreak = new TextLineBreak(textEndOfLine, flowDirection);
}
@@ -44,10 +42,8 @@ namespace Avalonia.Media.TextFormatting
{
case TextWrapping.NoWrap:
{
- TextLineImpl.SortRuns(shapedRuns);
-
- textLine = new TextLineImpl(shapedRuns, textRange, paragraphWidth, paragraphProperties,
- flowDirection, nextLineBreak);
+ textLine = new TextLineImpl(drawableTextRuns, firstTextSourceIndex, textSourceLength,
+ paragraphWidth, paragraphProperties, flowDirection, nextLineBreak);
textLine.FinalizeLine();
@@ -56,7 +52,7 @@ namespace Avalonia.Media.TextFormatting
case TextWrapping.WrapWithOverflow:
case TextWrapping.Wrap:
{
- textLine = PerformTextWrapping(shapedRuns, textRange, paragraphWidth, paragraphProperties,
+ textLine = PerformTextWrapping(drawableTextRuns, firstTextSourceIndex, paragraphWidth, paragraphProperties,
flowDirection, nextLineBreak);
break;
}
@@ -73,7 +69,7 @@ namespace Avalonia.Media.TextFormatting
/// The text run's.
/// The length to split at.
/// The split text runs.
- internal static SplitResult> SplitShapedRuns(List textRuns, int length)
+ internal static SplitResult> SplitDrawableRuns(List textRuns, int length)
{
var currentLength = 0;
@@ -83,13 +79,14 @@ namespace Avalonia.Media.TextFormatting
if (currentLength + currentRun.Text.Length < length)
{
- currentLength += currentRun.Text.Length;
+ currentLength += currentRun.TextSourceLength;
+
continue;
}
var firstCount = currentRun.Text.Length >= 1 ? i + 1 : i;
- var first = new List(firstCount);
+ var first = new List(firstCount);
if (firstCount > 1)
{
@@ -103,7 +100,7 @@ namespace Avalonia.Media.TextFormatting
if (currentLength + currentRun.Text.Length == length)
{
- var second = secondCount > 0 ? new List(secondCount) : null;
+ var second = secondCount > 0 ? new List(secondCount) : null;
if (second != null)
{
@@ -117,15 +114,20 @@ namespace Avalonia.Media.TextFormatting
first.Add(currentRun);
- return new SplitResult>(first, second);
+ return new SplitResult>(first, second);
}
else
{
secondCount++;
- var second = new List(secondCount);
+ var second = new List(secondCount);
+
+ if (currentRun is not ShapedTextCharacters shapedTextCharacters)
+ {
+ throw new NotSupportedException("Only shaped runs can be split in between.");
+ }
- var split = currentRun.Split(length - currentLength);
+ var split = shapedTextCharacters.Split(length - currentLength);
first.Add(split.First);
@@ -136,32 +138,43 @@ namespace Avalonia.Media.TextFormatting
second.Add(textRuns[i + j]);
}
- return new SplitResult>(first, second);
+ return new SplitResult>(first, second);
}
}
- return new SplitResult>(textRuns, null);
+ return new SplitResult>(textRuns, null);
}
///
/// Shape specified text runs with specified paragraph embedding.
///
/// The text runs to shape.
- /// The paragraph embedding level.
+ /// The default paragraph properties.
/// The resolved flow direction.
///
/// A list of shaped text characters.
///
- private static List ShapeTextRuns(List textRuns,
- FlowDirection flowDirection, out FlowDirection resolvedFlowDirection)
+ private static List ShapeTextRuns(List textRuns, TextParagraphProperties paragraphProperties,
+ out FlowDirection resolvedFlowDirection)
{
- var shapedTextCharacters = new List();
+ var flowDirection = paragraphProperties.FlowDirection;
+ var drawableTextRuns = new List();
var biDiData = new BidiData((sbyte)flowDirection);
foreach (var textRun in textRuns)
{
- biDiData.Append(textRun.Text);
+ if (textRun.Text.IsEmpty)
+ {
+ var text = new char[textRun.TextSourceLength];
+
+ biDiData.Append(text);
+ }
+ else
+ {
+ biDiData.Append(textRun.Text);
+ }
+
}
var biDi = BidiAlgorithm.Instance.Value!;
@@ -173,68 +186,90 @@ namespace Avalonia.Media.TextFormatting
resolvedFlowDirection =
(resolvedEmbeddingLevel & 1) == 0 ? FlowDirection.LeftToRight : FlowDirection.RightToLeft;
- var shapeableRuns = new List(textRuns.Count);
+ var processedRuns = new List(textRuns.Count);
foreach (var coalescedRuns in CoalesceLevels(textRuns, biDi.ResolvedLevels))
{
- shapeableRuns.AddRange(coalescedRuns);
+ processedRuns.AddRange(coalescedRuns);
}
- for (var index = 0; index < shapeableRuns.Count; index++)
+ for (var index = 0; index < processedRuns.Count; index++)
{
- var currentRun = shapeableRuns[index];
- var groupedRuns = new List(2) { currentRun };
- var text = currentRun.Text;
- var start = currentRun.Text.Start;
- var length = currentRun.Text.Length;
- var bufferOffset = currentRun.Text.BufferOffset;
-
- while (index + 1 < shapeableRuns.Count)
- {
- var nextRun = shapeableRuns[index + 1];
-
- if (currentRun.CanShapeTogether(nextRun))
- {
- groupedRuns.Add(nextRun);
+ var currentRun = processedRuns[index];
- length += nextRun.Text.Length;
-
- if (start > nextRun.Text.Start)
+ switch (currentRun)
+ {
+ case DrawableTextRun drawableRun:
{
- start = nextRun.Text.Start;
+ drawableTextRuns.Add(drawableRun);
+
+ break;
}
- if (bufferOffset > nextRun.Text.BufferOffset)
+ case ShapeableTextCharacters shapeableRun:
{
- bufferOffset = nextRun.Text.BufferOffset;
- }
+ var groupedRuns = new List(2) { shapeableRun };
+ var text = currentRun.Text;
+ var start = currentRun.Text.Start;
+ var length = currentRun.Text.Length;
+ var bufferOffset = currentRun.Text.BufferOffset;
- text = new ReadOnlySlice(text.Buffer, start, length, bufferOffset);
-
- index++;
+ while (index + 1 < processedRuns.Count)
+ {
+ if (processedRuns[index + 1] is not ShapeableTextCharacters nextRun)
+ {
+ break;
+ }
- currentRun = nextRun;
+ if (shapeableRun.CanShapeTogether(nextRun))
+ {
+ groupedRuns.Add(nextRun);
- continue;
- }
+ length += nextRun.Text.Length;
- break;
- }
+ if (start > nextRun.Text.Start)
+ {
+ start = nextRun.Text.Start;
+ }
+
+ if (bufferOffset > nextRun.Text.BufferOffset)
+ {
+ bufferOffset = nextRun.Text.BufferOffset;
+ }
+
+ text = new ReadOnlySlice(text.Buffer, start, length, bufferOffset);
- shapedTextCharacters.AddRange(ShapeTogether(groupedRuns, text));
+ index++;
+
+ shapeableRun = nextRun;
+
+ continue;
+ }
+
+ break;
+ }
+
+ var shaperOptions = new TextShaperOptions(currentRun.Properties!.Typeface.GlyphTypeface,
+ currentRun.Properties.FontRenderingEmSize,
+ shapeableRun.BidiLevel, currentRun.Properties.CultureInfo, paragraphProperties.DefaultIncrementalTab);
+
+ drawableTextRuns.AddRange(ShapeTogether(groupedRuns, text, shaperOptions));
+
+ break;
+ }
+ }
}
- return shapedTextCharacters;
+ return drawableTextRuns;
}
private static IReadOnlyList ShapeTogether(
- IReadOnlyList textRuns, ReadOnlySlice text)
+ IReadOnlyList textRuns, ReadOnlySlice text, TextShaperOptions options)
{
var shapedRuns = new List(textRuns.Count);
var firstRun = textRuns[0];
- var shapedBuffer = TextShaper.Current.ShapeText(text, firstRun.Properties.Typeface.GlyphTypeface,
- firstRun.Properties.FontRenderingEmSize, firstRun.Properties.CultureInfo, firstRun.BidiLevel);
+ var shapedBuffer = TextShaper.Current.ShapeText(text, options);
for (var i = 0; i < textRuns.Count; i++)
{
@@ -256,8 +291,8 @@ namespace Avalonia.Media.TextFormatting
/// The text characters to form from.
/// The bidi levels.
///
- private static IEnumerable> CoalesceLevels(
- IReadOnlyList textCharacters,
+ private static IEnumerable> CoalesceLevels(
+ IReadOnlyList textCharacters,
ReadOnlySlice levels)
{
if (levels.Length == 0)
@@ -275,7 +310,19 @@ namespace Avalonia.Media.TextFormatting
for (var i = 0; i < textCharacters.Count; i++)
{
var j = 0;
- currentRun = textCharacters[i];
+ currentRun = textCharacters[i] as TextCharacters;
+
+ if (currentRun == null)
+ {
+ var drawableRun = textCharacters[i];
+
+ yield return new[] { drawableRun };
+
+ levelIndex += drawableRun.TextSourceLength;
+
+ continue;
+ }
+
runText = currentRun.Text;
for (; j < runText.Length;)
@@ -330,18 +377,18 @@ namespace Avalonia.Media.TextFormatting
/// The text source.
/// The first text source index.
///
- ///
+ ///
///
/// The formatted text runs.
///
- private static List FetchTextRuns(ITextSource textSource, int firstTextSourceIndex,
- out TextEndOfLine? endOfLine, out TextRange textRange)
+ private static List FetchTextRuns(ITextSource textSource, int firstTextSourceIndex,
+ out TextEndOfLine? endOfLine, out int textSourceLength)
{
- var length = 0;
+ textSourceLength = 0;
endOfLine = null;
- var textRuns = new List();
+ var textRuns = new List();
var textRunEnumerator = new TextRunEnumerator(textSource, firstTextSourceIndex);
@@ -349,8 +396,19 @@ namespace Avalonia.Media.TextFormatting
{
var textRun = textRunEnumerator.Current;
- if(textRun == null)
+ if (textRun == null)
+ {
+ break;
+ }
+
+ if (textRun is TextEndOfLine textEndOfLine)
{
+ endOfLine = textEndOfLine;
+
+ textRuns.Add(textRun);
+
+ textSourceLength += textRun.TextSourceLength;
+
break;
}
@@ -365,9 +423,7 @@ namespace Avalonia.Media.TextFormatting
textRuns.Add(splitResult);
- length += runLineBreak.PositionWrap;
-
- textRange = new TextRange(firstTextSourceIndex, length);
+ textSourceLength += runLineBreak.PositionWrap;
return textRuns;
}
@@ -376,16 +432,16 @@ namespace Avalonia.Media.TextFormatting
break;
}
- case TextEndOfLine textEndOfLine:
- endOfLine = textEndOfLine;
- break;
+ case DrawableTextRun drawableTextRun:
+ {
+ textRuns.Add(drawableTextRun);
+ break;
+ }
}
- length += textRun.Text.Length;
+ textSourceLength += textRun.TextSourceLength;
}
- textRange = new TextRange(firstTextSourceIndex, length);
-
return textRuns;
}
@@ -415,48 +471,74 @@ namespace Avalonia.Media.TextFormatting
return false;
}
- private static int MeasureLength(IReadOnlyList textRuns, TextRange textRange,
- double paragraphWidth)
+ private static bool TryMeasureLength(IReadOnlyList textRuns, int firstTextSourceIndex, double paragraphWidth, out int measuredLength)
{
+ measuredLength = 0;
var currentWidth = 0.0;
- var lastCluster = textRange.Start;
+ var lastCluster = firstTextSourceIndex;
foreach (var currentRun in textRuns)
{
- for (var i = 0; i < currentRun.ShapedBuffer.Length; i++)
+ switch (currentRun)
{
- var glyphInfo = currentRun.ShapedBuffer[i];
+ case ShapedTextCharacters shapedTextCharacters:
+ {
+ for (var i = 0; i < shapedTextCharacters.ShapedBuffer.Length; i++)
+ {
+ var glyphInfo = shapedTextCharacters.ShapedBuffer[i];
- if (currentWidth + glyphInfo.GlyphAdvance > paragraphWidth)
- {
- var measuredLength = lastCluster - textRange.Start;
+ if (currentWidth + glyphInfo.GlyphAdvance > paragraphWidth)
+ {
+ goto found;
+ }
- return measuredLength == 0 ? 1 : measuredLength;
- }
+ lastCluster = glyphInfo.GlyphCluster;
+ currentWidth += glyphInfo.GlyphAdvance;
+ }
+
+ break;
+ }
+
+ case { } drawableTextRun:
+ {
+ if (currentWidth + drawableTextRun.Size.Width > paragraphWidth)
+ {
+ goto found;
+ }
- lastCluster = glyphInfo.GlyphCluster;
- currentWidth += glyphInfo.GlyphAdvance;
+ lastCluster += currentRun.TextSourceLength;
+ currentWidth += currentRun.Size.Width;
+
+ break;
+ }
}
}
- return textRange.Length;
+ found:
+
+ measuredLength = Math.Max(0, lastCluster - firstTextSourceIndex + 1);
+
+ return measuredLength != 0;
}
///
/// Performs text wrapping returns a list of text lines.
///
///
- /// The text range that is covered by the text runs.
+ /// The first text source index.
/// The paragraph width.
/// The text paragraph properties.
///
/// The current line break if the line was explicitly broken.
/// The wrapped text line.
- private static TextLineImpl PerformTextWrapping(List textRuns, TextRange textRange,
+ private static TextLineImpl PerformTextWrapping(List textRuns, int firstTextSourceIndex,
double paragraphWidth, TextParagraphProperties paragraphProperties, FlowDirection flowDirection,
TextLineBreak? currentLineBreak)
{
- var measuredLength = MeasureLength(textRuns, textRange, paragraphWidth);
+ if (!TryMeasureLength(textRuns, firstTextSourceIndex, paragraphWidth, out var measuredLength))
+ {
+ measuredLength = 1;
+ }
var currentLength = 0;
@@ -568,9 +650,7 @@ namespace Avalonia.Media.TextFormatting
break;
}
- var splitResult = SplitShapedRuns(textRuns, measuredLength);
-
- textRange = new TextRange(textRange.Start, measuredLength);
+ var splitResult = SplitDrawableRuns(textRuns, measuredLength);
var remainingCharacters = splitResult.Second;
@@ -583,9 +663,8 @@ namespace Avalonia.Media.TextFormatting
lineBreak = new TextLineBreak(currentLineBreak.TextEndOfLine, flowDirection);
}
- TextLineImpl.SortRuns(splitResult.First);
-
- var textLine = new TextLineImpl(splitResult.First, textRange, paragraphWidth, paragraphProperties, flowDirection,
+ var textLine = new TextLineImpl(splitResult.First, firstTextSourceIndex, measuredLength,
+ paragraphWidth, paragraphProperties, flowDirection,
lineBreak);
return textLine.FinalizeLine();
@@ -644,7 +723,9 @@ namespace Avalonia.Media.TextFormatting
var cultureInfo = textRun.Properties.CultureInfo;
- var shapedBuffer = textShaper.ShapeText(textRun.Text, glyphTypeface, fontRenderingEmSize, cultureInfo, (sbyte)flowDirection);
+ var shaperOptions = new TextShaperOptions(glyphTypeface, fontRenderingEmSize, (sbyte)flowDirection, cultureInfo);
+
+ var shapedBuffer = textShaper.ShapeText(textRun.Text, shaperOptions);
return new ShapedTextCharacters(shapedBuffer, textRun.Properties);
}
diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextLayout.cs b/src/Avalonia.Visuals/Media/TextFormatting/TextLayout.cs
index 0ff127694b..e3bcdee014 100644
--- a/src/Avalonia.Visuals/Media/TextFormatting/TextLayout.cs
+++ b/src/Avalonia.Visuals/Media/TextFormatting/TextLayout.cs
@@ -162,7 +162,9 @@ namespace Avalonia.Media.TextFormatting
foreach (var textLine in TextLines)
{
- if (textLine.TextRange.End < textPosition)
+ var end = textLine.FirstTextSourceIndex + textLine.Length - 1;
+
+ if (end < textPosition)
{
currentY += textLine.Height;
@@ -193,187 +195,34 @@ namespace Avalonia.Media.TextFormatting
var result = new List(TextLines.Count);
var currentY = 0d;
- var currentPosition = 0;
- var currentRect = Rect.Empty;
foreach (var textLine in TextLines)
{
//Current line isn't covered.
- if (currentPosition + textLine.TextRange.Length <= start)
+ if (textLine.FirstTextSourceIndex + textLine.Length <= start)
{
currentY += textLine.Height;
- currentPosition += textLine.TextRange.Length;
continue;
}
- //The whole line is covered.
- if (currentPosition >= start && start + length > currentPosition + textLine.TextRange.Length)
- {
- currentRect = new Rect(textLine.Start, currentY, textLine.WidthIncludingTrailingWhitespace,
- textLine.Height);
-
- result.Add(currentRect);
-
- currentY += textLine.Height;
- currentPosition += textLine.TextRange.Length;
-
- continue;
- }
-
- var startX = textLine.Start;
-
- //A portion of the line is covered.
- for (var index = 0; index < textLine.TextRuns.Count; index++)
- {
- var currentRun = (ShapedTextCharacters)textLine.TextRuns[index];
- ShapedTextCharacters? nextRun = null;
+ var textBounds = textLine.GetTextBounds(start, length);
- if (index + 1 < textLine.TextRuns.Count)
- {
- nextRun = (ShapedTextCharacters)textLine.TextRuns[index + 1];
- }
-
- if (nextRun != null)
- {
- if (nextRun.Text.Start < currentRun.Text.Start && start + length < currentRun.Text.End)
- {
- goto skip;
- }
-
- if (currentRun.Text.Start >= start + length)
- {
- goto skip;
- }
-
- if (currentRun.Text.Start > nextRun.Text.Start && currentRun.Text.Start < start)
- {
- goto skip;
- }
-
- if (currentRun.Text.End < start)
- {
- goto skip;
- }
-
- goto noop;
-
- skip:
- {
- startX += currentRun.Size.Width;
-
- currentPosition = currentRun.Text.Start;
- }
-
- continue;
-
- noop:{ }
- }
-
- var endOffset = currentRun.GlyphRun.GetDistanceFromCharacterHit(
- currentRun.ShapedBuffer.IsLeftToRight ?
- new CharacterHit(start + length) :
- new CharacterHit(start));
-
- var endX = startX + endOffset;
-
- var startOffset = currentRun.GlyphRun.GetDistanceFromCharacterHit(
- currentRun.ShapedBuffer.IsLeftToRight ?
- new CharacterHit(start) :
- new CharacterHit(start + length));
-
- startX += startOffset;
-
- var characterHit = currentRun.GlyphRun.IsLeftToRight ?
- currentRun.GlyphRun.GetCharacterHitFromDistance(endOffset, out _) :
- currentRun.GlyphRun.GetCharacterHitFromDistance(startOffset, out _);
-
- currentPosition = characterHit.FirstCharacterIndex + characterHit.TrailingLength;
-
- if(nextRun != null)
- {
- if (currentRun.ShapedBuffer.IsLeftToRight == nextRun.ShapedBuffer.IsLeftToRight)
- {
- endOffset = nextRun.GlyphRun.GetDistanceFromCharacterHit(
- nextRun.ShapedBuffer.IsLeftToRight ?
- new CharacterHit(start + length) :
- new CharacterHit(start));
-
- index++;
-
- endX += endOffset;
-
- currentRun = nextRun;
-
- if (currentRun.ShapedBuffer.IsLeftToRight)
- {
- characterHit = nextRun.GlyphRun.GetCharacterHitFromDistance(endOffset, out _);
-
- currentPosition = characterHit.FirstCharacterIndex + characterHit.TrailingLength;
- }
- }
- }
-
- if (endX < startX)
- {
- (endX, startX) = (startX, endX);
- }
-
- var width = endX - startX;
+ foreach (var bounds in textBounds)
+ {
+ Rect? last = result.Count > 0 ? result[result.Count - 1] : null;
- if (result.Count > 0 && MathUtilities.AreClose(currentRect.Top, currentY) &&
- MathUtilities.AreClose(currentRect.Right, startX))
- {
- result[result.Count - 1] = currentRect.WithWidth(currentRect.Width + width);
- }
- else
- {
- currentRect = new Rect(startX, currentY, width, textLine.Height);
-
- result.Add(currentRect);
- }
-
- if (currentRun.ShapedBuffer.IsLeftToRight)
+ if (last.HasValue && MathUtilities.AreClose(last.Value.Right, bounds.Rectangle.Left) && MathUtilities.AreClose(last.Value.Top, currentY))
{
- if (nextRun != null)
- {
- if (nextRun.Text.Start > currentRun.Text.Start && nextRun.Text.Start >= start + length)
- {
- break;
- }
-
- currentPosition = nextRun.Text.End;
- }
- else
- {
- if (currentPosition >= start + length)
- {
- break;
- }
- }
+ result[result.Count - 1] = last.Value.WithWidth(last.Value.Width + bounds.Rectangle.Width);
}
else
{
- if (currentPosition <= start)
- {
- break;
- }
- }
-
- if (!currentRun.ShapedBuffer.IsLeftToRight && currentPosition != currentRun.Text.Start)
- {
- endX += currentRun.GlyphRun.Size.Width - endOffset;
- }
-
- startX = endX;
+ result.Add(bounds.Rectangle.WithY(currentY));
+ }
}
- if (currentPosition == start || currentPosition == start + length)
- {
- break;
- }
-
- if (textLine.TextRange.Start + textLine.TextRange.Length >= start + length)
+ if(textLine.FirstTextSourceIndex + textLine.Length >= start + length)
{
break;
}
@@ -433,12 +282,13 @@ namespace Avalonia.Media.TextFormatting
{
var textLine = TextLines[index];
- if (textLine.TextRange.Start + textLine.TextRange.Length < charIndex)
+ if (textLine.FirstTextSourceIndex + textLine.Length < charIndex)
{
continue;
}
- if (charIndex >= textLine.TextRange.Start && charIndex <= textLine.TextRange.End + (trailingEdge ? 1 : 0))
+ if (charIndex >= textLine.FirstTextSourceIndex &&
+ charIndex <= textLine.FirstTextSourceIndex + textLine.Length - (trailingEdge ? 0 : 1))
{
return index;
}
@@ -451,11 +301,11 @@ namespace Avalonia.Media.TextFormatting
{
var (x, y) = point;
- var lastTrailingIndex = textLine.TextRange.Start + textLine.TextRange.Length;
+ var lastTrailingIndex = textLine.FirstTextSourceIndex + textLine.Length;
var isInside = x >= 0 && x <= textLine.Width && y >= 0 && y <= textLine.Height;
- if (x >= textLine.Width && textLine.TextRange.Length > 0 && textLine.NewLineLength > 0)
+ if (x >= textLine.Width && textLine.Length > 0 && textLine.NewLineLength > 0)
{
lastTrailingIndex -= textLine.NewLineLength;
}
@@ -465,7 +315,7 @@ namespace Avalonia.Media.TextFormatting
var isTrailing = lastTrailingIndex == textPosition && characterHit.TrailingLength > 0 ||
y > Bounds.Bottom;
- if (textPosition == textLine.TextRange.Start + textLine.TextRange.Length)
+ if (textPosition == textLine.FirstTextSourceIndex + textLine.Length)
{
textPosition -= textLine.NewLineLength;
}
@@ -529,23 +379,21 @@ namespace Avalonia.Media.TextFormatting
/// Creates an empty text line.
///
/// The empty text line.
- private TextLine CreateEmptyTextLine(int startingIndex)
+ private TextLine CreateEmptyTextLine(int firstTextSourceIndex)
{
var flowDirection = _paragraphProperties.FlowDirection;
var properties = _paragraphProperties.DefaultTextRunProperties;
var glyphTypeface = properties.Typeface.GlyphTypeface;
- var text = new ReadOnlySlice(s_empty, startingIndex, 1);
+ var text = new ReadOnlySlice(s_empty, firstTextSourceIndex, 1);
var glyph = glyphTypeface.GetGlyph(s_empty[0]);
- var glyphInfos = new[] { new GlyphInfo(glyph, startingIndex) };
+ 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) };
-
- var textRange = new TextRange(startingIndex, 1);
+ var textRuns = new List { new ShapedTextCharacters(shapedBuffer, properties) };
- return new TextLineImpl(textRuns, textRange, MaxWidth, _paragraphProperties, flowDirection).FinalizeLine();
+ return new TextLineImpl(textRuns, firstTextSourceIndex, 1, MaxWidth, _paragraphProperties, flowDirection).FinalizeLine();
}
private IReadOnlyList CreateTextLines()
@@ -576,13 +424,13 @@ namespace Avalonia.Media.TextFormatting
_paragraphProperties, previousLine?.TextLineBreak);
#if DEBUG
- if (textLine.TextRange.Length == 0)
+ if (textLine.Length == 0)
{
throw new InvalidOperationException($"{nameof(textLine)} should not be empty.");
}
#endif
- currentPosition += textLine.TextRange.Length;
+ currentPosition += textLine.Length;
//Fulfill max height constraint
if (textLines.Count > 0 && !double.IsPositiveInfinity(MaxHeight) && height + textLine.Height > MaxHeight)
diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextLeadingPrefixCharacterEllipsis.cs b/src/Avalonia.Visuals/Media/TextFormatting/TextLeadingPrefixCharacterEllipsis.cs
index 74c4573630..5a14eda245 100644
--- a/src/Avalonia.Visuals/Media/TextFormatting/TextLeadingPrefixCharacterEllipsis.cs
+++ b/src/Avalonia.Visuals/Media/TextFormatting/TextLeadingPrefixCharacterEllipsis.cs
@@ -17,7 +17,7 @@ namespace Avalonia.Media.TextFormatting
/// Text used as collapsing symbol.
/// Length of leading prefix.
/// width in which collapsing is constrained to
- /// text run properties of ellispis symbol
+ /// text run properties of ellipsis symbol
public TextLeadingPrefixCharacterEllipsis(
ReadOnlySlice ellipsis,
int prefixLength,
@@ -35,16 +35,14 @@ namespace Avalonia.Media.TextFormatting
}
///
- public sealed override double Width { get; }
+ public override double Width { get; }
///
- public sealed override TextRun Symbol { get; }
+ public override TextRun Symbol { get; }
- public override IReadOnlyList? Collapse(TextLine textLine)
+ public override List? Collapse(TextLine textLine)
{
- var shapedTextRuns = textLine.TextRuns as List;
-
- if (shapedTextRuns is null)
+ if (textLine.TextRuns is not List textRuns || textRuns.Count == 0)
{
return null;
}
@@ -55,84 +53,103 @@ namespace Avalonia.Media.TextFormatting
if (Width < shapedSymbol.GlyphRun.Size.Width)
{
- return new List(0);
+ return new List(0);
}
// Overview of ellipsis structure
// Prefix length run | Ellipsis symbol | Post split run growing from the end |
var availableWidth = Width - shapedSymbol.Size.Width;
- while (runIndex < shapedTextRuns.Count)
+ while (runIndex < textRuns.Count)
{
- var currentRun = shapedTextRuns[runIndex];
-
- currentWidth += currentRun.Size.Width;
+ var currentRun = textRuns[runIndex];
- if (currentWidth > availableWidth)
+ switch (currentRun)
{
- currentRun.TryMeasureCharacters(availableWidth, out var measuredLength);
-
- var shapedTextCharacters = new List(shapedTextRuns.Count);
-
- if (measuredLength > 0)
+ case ShapedTextCharacters shapedRun:
{
- List? preSplitRuns = null;
- List? postSplitRuns = null;
+ currentWidth += currentRun.Size.Width;
- if (_prefixLength > 0)
+ if (currentWidth > availableWidth)
{
- var splitResult = TextFormatterImpl.SplitShapedRuns(shapedTextRuns, Math.Min(_prefixLength, measuredLength));
+ shapedRun.TryMeasureCharacters(availableWidth, out var measuredLength);
- shapedTextCharacters.AddRange(splitResult.First);
+ var collapsedRuns = new List(textRuns.Count);
- TextLineImpl.SortRuns(shapedTextCharacters);
-
- preSplitRuns = splitResult.First;
- postSplitRuns = splitResult.Second;
- }
- else
- {
- postSplitRuns = shapedTextRuns;
- }
+ if (measuredLength > 0)
+ {
+ List? preSplitRuns = null;
+ List? postSplitRuns;
- shapedTextCharacters.Add(shapedSymbol);
+ if (_prefixLength > 0)
+ {
+ var splitResult = TextFormatterImpl.SplitDrawableRuns(textRuns,
+ Math.Min(_prefixLength, measuredLength));
- if (measuredLength > _prefixLength && postSplitRuns is not null)
- {
- var availableSuffixWidth = availableWidth;
+ collapsedRuns.AddRange(splitResult.First);
- if (preSplitRuns is not null)
- {
- foreach (var run in preSplitRuns)
+ preSplitRuns = splitResult.First;
+ postSplitRuns = splitResult.Second;
+ }
+ else
{
- availableSuffixWidth -= run.Size.Width;
+ postSplitRuns = textRuns;
}
- }
- for (int i = postSplitRuns.Count - 1; i >= 0; i--)
- {
- var run = postSplitRuns[i];
+ collapsedRuns.Add(shapedSymbol);
- if (run.TryMeasureCharactersBackwards(availableSuffixWidth, out int suffixCount, out double suffixWidth))
+ if (measuredLength <= _prefixLength || postSplitRuns is null)
{
- availableSuffixWidth -= suffixWidth;
+ return collapsedRuns;
+ }
+
+ var availableSuffixWidth = availableWidth;
- if (suffixCount > 0)
+ if (preSplitRuns is not null)
+ {
+ foreach (var run in preSplitRuns)
{
- var splitSuffix = run.Split(run.TextSourceLength - suffixCount);
+ availableSuffixWidth -= run.Size.Width;
+ }
+ }
+
+ for (var i = postSplitRuns.Count - 1; i >= 0; i--)
+ {
+ var run = postSplitRuns[i];
- shapedTextCharacters.Add(splitSuffix.Second!);
+ switch (run)
+ {
+ case ShapedTextCharacters endShapedRun:
+ {
+ if (endShapedRun.TryMeasureCharactersBackwards(availableSuffixWidth,
+ out var suffixCount, out var suffixWidth))
+ {
+ availableSuffixWidth -= suffixWidth;
+
+ if (suffixCount > 0)
+ {
+ var splitSuffix =
+ endShapedRun.Split(run.TextSourceLength - suffixCount);
+
+ collapsedRuns.Add(splitSuffix.Second!);
+ }
+ }
+
+ break;
+ }
}
}
}
+ else
+ {
+ collapsedRuns.Add(shapedSymbol);
+ }
+
+ return collapsedRuns;
}
- }
- else
- {
- shapedTextCharacters.Add(shapedSymbol);
- }
- return shapedTextCharacters;
+ break;
+ }
}
availableWidth -= currentRun.Size.Width;
diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextLine.cs b/src/Avalonia.Visuals/Media/TextFormatting/TextLine.cs
index 130d0e9c39..1f69c15acc 100644
--- a/src/Avalonia.Visuals/Media/TextFormatting/TextLine.cs
+++ b/src/Avalonia.Visuals/Media/TextFormatting/TextLine.cs
@@ -16,13 +16,9 @@ namespace Avalonia.Media.TextFormatting
///
public abstract IReadOnlyList TextRuns { get; }
- ///
- /// Gets the text range that is covered by the line.
- ///
- ///
- /// The text range that is covered by the line.
- ///
- public abstract TextRange TextRange { get; }
+ public abstract int FirstTextSourceIndex { get; }
+
+ public abstract int Length { get; }
///
/// Gets the state of the line when broken by line breaking process.
@@ -189,6 +185,14 @@ namespace Avalonia.Media.TextFormatting
/// The after backspacing.
public abstract CharacterHit GetBackspaceCaretCharacterHit(CharacterHit characterHit);
+ ///
+ /// Get an array of bounding rectangles of a range of characters within a text line.
+ ///
+ /// index of first character of specified range
+ /// number of characters of the specified range
+ /// an array of bounding rectangles.
+ public abstract IReadOnlyList GetTextBounds(int firstTextSourceCharacterIndex, int textLength);
+
///
/// Gets the text line offset x.
///
diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextLineBreak.cs b/src/Avalonia.Visuals/Media/TextFormatting/TextLineBreak.cs
index be9661c2bf..ce35e47fbd 100644
--- a/src/Avalonia.Visuals/Media/TextFormatting/TextLineBreak.cs
+++ b/src/Avalonia.Visuals/Media/TextFormatting/TextLineBreak.cs
@@ -5,11 +5,11 @@ namespace Avalonia.Media.TextFormatting
public class TextLineBreak
{
public TextLineBreak(TextEndOfLine? textEndOfLine = null, FlowDirection flowDirection = FlowDirection.LeftToRight,
- IReadOnlyList? remainingCharacters = null)
+ IReadOnlyList? remainingRuns = null)
{
TextEndOfLine = textEndOfLine;
FlowDirection = flowDirection;
- RemainingCharacters = remainingCharacters;
+ RemainingRuns = remainingRuns;
}
///
@@ -23,8 +23,8 @@ namespace Avalonia.Media.TextFormatting
public FlowDirection FlowDirection { get; }
///
- /// Get the remaining shaped characters that were split up by the during the formatting process.
+ /// Get the remaining runs that were split up by the during the formatting process.
///
- public IReadOnlyList? RemainingCharacters { get; }
+ public IReadOnlyList? RemainingRuns { get; }
}
}
diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextLineImpl.cs b/src/Avalonia.Visuals/Media/TextFormatting/TextLineImpl.cs
index 49bee6e776..30e3728d1f 100644
--- a/src/Avalonia.Visuals/Media/TextFormatting/TextLineImpl.cs
+++ b/src/Avalonia.Visuals/Media/TextFormatting/TextLineImpl.cs
@@ -1,32 +1,27 @@
using System;
using System.Collections.Generic;
-using Avalonia.Media.TextFormatting.Unicode;
using Avalonia.Utilities;
namespace Avalonia.Media.TextFormatting
{
internal class TextLineImpl : TextLine
{
- private static readonly Comparer s_compareStart = Comparer.Default;
-
- private static readonly Comparison s_compareLogicalOrder =
- (a, b) => s_compareStart.Compare(a.Text.Start, b.Text.Start);
-
- private readonly List _textRuns;
+ private readonly List _textRuns;
private readonly double _paragraphWidth;
private readonly TextParagraphProperties _paragraphProperties;
private TextLineMetrics _textLineMetrics;
private readonly FlowDirection _flowDirection;
- public TextLineImpl(List textRuns, TextRange textRange, double paragraphWidth,
- TextParagraphProperties paragraphProperties, FlowDirection flowDirection = FlowDirection.LeftToRight,
+ public TextLineImpl(List textRuns, int firstTextSourceIndex, int length, double paragraphWidth,
+ TextParagraphProperties paragraphProperties, FlowDirection flowDirection = FlowDirection.LeftToRight,
TextLineBreak? lineBreak = null, bool hasCollapsed = false)
{
- TextRange = textRange;
+ FirstTextSourceIndex = firstTextSourceIndex;
+ Length = length;
TextLineBreak = lineBreak;
HasCollapsed = hasCollapsed;
- _textRuns = textRuns;
+ _textRuns = textRuns;
_paragraphWidth = paragraphWidth;
_paragraphProperties = paragraphProperties;
@@ -37,7 +32,10 @@ namespace Avalonia.Media.TextFormatting
public override IReadOnlyList TextRuns => _textRuns;
///
- public override TextRange TextRange { get; }
+ public override int FirstTextSourceIndex { get; }
+
+ ///
+ public override int Length { get; }
///
public override TextLineBreak? TextLineBreak { get; }
@@ -88,7 +86,7 @@ namespace Avalonia.Media.TextFormatting
foreach (var textRun in _textRuns)
{
- var offsetY = Baseline - textRun.GlyphRun.BaselineOrigin.Y;
+ var offsetY = GetBaselineOffset(this, textRun);
textRun.Draw(drawingContext, new Point(currentX, currentY + offsetY));
@@ -96,6 +94,30 @@ namespace Avalonia.Media.TextFormatting
}
}
+ private static double GetBaselineOffset(TextLine textLine, DrawableTextRun textRun)
+ {
+ var baseline = textRun.Baseline;
+ var baselineAlignment = textRun.Properties?.BaselineAlignment;
+
+ switch (baselineAlignment)
+ {
+ case BaselineAlignment.Top:
+ return 0;
+ case BaselineAlignment.Center:
+ return textLine.Height / 2 - textRun.Size.Height / 2;
+ case BaselineAlignment.Bottom:
+ return textLine.Height - textRun.Size.Height;
+ case BaselineAlignment.Baseline:
+ case BaselineAlignment.TextTop:
+ case BaselineAlignment.TextBottom:
+ case BaselineAlignment.Subscript:
+ case BaselineAlignment.Superscript:
+ return textLine.Baseline - baseline;
+ default:
+ throw new ArgumentOutOfRangeException(nameof(baselineAlignment), baselineAlignment, null);
+ }
+ }
+
///
public override TextLine Collapse(params TextCollapsingProperties[] collapsingPropertiesList)
{
@@ -108,47 +130,82 @@ namespace Avalonia.Media.TextFormatting
var collapsedRuns = collapsingProperties.Collapse(this);
- if (collapsedRuns is List shapedRuns)
+ if (collapsedRuns is null)
{
- var collapsedLine = new TextLineImpl(shapedRuns, TextRange, _paragraphWidth, _paragraphProperties, _flowDirection, TextLineBreak, true);
+ return this;
+ }
- if (shapedRuns.Count > 0)
- {
- collapsedLine.FinalizeLine();
- }
+ var collapsedLine = new TextLineImpl(collapsedRuns, FirstTextSourceIndex, Length, _paragraphWidth, _paragraphProperties,
+ _flowDirection, TextLineBreak, true);
- return collapsedLine;
+ if (collapsedRuns.Count > 0)
+ {
+ collapsedLine.FinalizeLine();
}
- return this;
+ return collapsedLine;
+
}
///
public override CharacterHit GetCharacterHitFromDistance(double distance)
{
+ if (_textRuns.Count == 0)
+ {
+ return new CharacterHit();
+ }
+
distance -= Start;
-
+
if (distance <= 0)
{
// hit happens before the line, return the first position
var firstRun = _textRuns[0];
- return firstRun.GlyphRun.GetCharacterHitFromDistance(distance, out _);
+ if (firstRun is ShapedTextCharacters shapedTextCharacters)
+ {
+ return shapedTextCharacters.GlyphRun.GetCharacterHitFromDistance(distance, out _);
+ }
+
+ return _flowDirection == FlowDirection.LeftToRight ?
+ new CharacterHit(FirstTextSourceIndex) :
+ new CharacterHit(FirstTextSourceIndex + Length);
}
// process hit that happens within the line
var characterHit = new CharacterHit();
+ var currentPosition = FirstTextSourceIndex;
- foreach (var run in _textRuns)
+ foreach (var currentRun in _textRuns)
{
- characterHit = run.GlyphRun.GetCharacterHitFromDistance(distance, out _);
+ switch (currentRun)
+ {
+ case ShapedTextCharacters shapedRun:
+ {
+ characterHit = shapedRun.GlyphRun.GetCharacterHitFromDistance(distance, out _);
+ break;
+ }
+ default:
+ {
+ if (distance < currentRun.Size.Width / 2)
+ {
+ characterHit = new CharacterHit(currentPosition);
+ }
+ else
+ {
+ characterHit = new CharacterHit(currentPosition, currentRun.TextSourceLength);
+ }
+ break;
+ }
+ }
- if (distance <= run.Size.Width)
+ if (distance <= currentRun.Size.Width)
{
break;
}
- distance -= run.Size.Width;
+ distance -= currentRun.Size.Width;
+ currentPosition += currentRun.TextSourceLength;
}
return characterHit;
@@ -158,91 +215,119 @@ namespace Avalonia.Media.TextFormatting
public override double GetDistanceFromCharacterHit(CharacterHit characterHit)
{
var characterIndex = characterHit.FirstCharacterIndex + (characterHit.TrailingLength != 0 ? 1 : 0);
-
var currentDistance = Start;
+ var currentPosition = FirstTextSourceIndex;
GlyphRun? lastRun = null;
for (var index = 0; index < _textRuns.Count; index++)
{
var textRun = _textRuns[index];
- var currentRun = textRun.GlyphRun;
- if (lastRun != null)
+ switch (textRun)
{
- if (!lastRun.IsLeftToRight && currentRun.IsLeftToRight &&
- currentRun.Characters.Start == characterHit.FirstCharacterIndex &&
- characterHit.TrailingLength == 0)
- {
- return currentDistance;
- }
- }
-
- //Look for a hit in within the current run
- if (characterIndex >= textRun.Text.Start && characterIndex <= textRun.Text.End)
- {
- var distance = currentRun.GetDistanceFromCharacterHit(characterHit);
+ case ShapedTextCharacters shapedTextCharacters:
+ {
+ var currentRun = shapedTextCharacters.GlyphRun;
- return currentDistance + distance;
- }
+ if (lastRun != null)
+ {
+ if (!lastRun.IsLeftToRight && currentRun.IsLeftToRight &&
+ currentRun.Characters.Start == characterHit.FirstCharacterIndex &&
+ characterHit.TrailingLength == 0)
+ {
+ return currentDistance;
+ }
+ }
- //Look at the left and right edge of the current run
- if (currentRun.IsLeftToRight)
- {
- if (lastRun == null || lastRun.IsLeftToRight)
- {
- if (characterIndex <= textRun.Text.Start)
- {
- return currentDistance;
- }
- }
- else
- {
- if (characterIndex == textRun.Text.Start)
- {
- return currentDistance;
- }
- }
+ //Look for a hit in within the current run
+ if (characterIndex >= textRun.Text.Start && characterIndex <= textRun.Text.Start + textRun.Text.Length)
+ {
+ var distance = currentRun.GetDistanceFromCharacterHit(characterHit);
- if (characterIndex == textRun.Text.Start + textRun.Text.Length && characterHit.TrailingLength > 0)
- {
- return currentDistance + currentRun.Size.Width;
- }
- }
- else
- {
- if (characterIndex == textRun.Text.Start)
- {
- return currentDistance + currentRun.Size.Width;
- }
+ return currentDistance + distance;
+ }
- var nextRun = index + 1 < _textRuns.Count ? _textRuns[index + 1] : null;
+ //Look at the left and right edge of the current run
+ if (currentRun.IsLeftToRight)
+ {
+ if (lastRun == null || lastRun.IsLeftToRight)
+ {
+ if (characterIndex <= textRun.Text.Start)
+ {
+ return currentDistance;
+ }
+ }
+ else
+ {
+ if (characterIndex == textRun.Text.Start)
+ {
+ return currentDistance;
+ }
+ }
+
+ if (characterIndex == textRun.Text.Start + textRun.Text.Length &&
+ characterHit.TrailingLength > 0)
+ {
+ return currentDistance + currentRun.Size.Width;
+ }
+ }
+ else
+ {
+ if (characterIndex == textRun.Text.Start)
+ {
+ return currentDistance + currentRun.Size.Width;
+ }
+
+ var nextRun = index + 1 < _textRuns.Count ?
+ _textRuns[index + 1] as ShapedTextCharacters :
+ null;
+
+ if (nextRun != null)
+ {
+ if (characterHit.FirstCharacterIndex == textRun.Text.End &&
+ nextRun.ShapedBuffer.IsLeftToRight)
+ {
+ return currentDistance;
+ }
+
+ if (characterIndex > textRun.Text.End && nextRun.Text.End < textRun.Text.End)
+ {
+ return currentDistance;
+ }
+ }
+ else
+ {
+ if (characterIndex > textRun.Text.End)
+ {
+ return currentDistance;
+ }
+ }
+ }
- if (nextRun != null)
- {
- if (characterHit.FirstCharacterIndex == textRun.Text.End && nextRun.ShapedBuffer.IsLeftToRight)
- {
- return currentDistance;
- }
+ lastRun = currentRun;
- if (characterIndex > textRun.Text.End && nextRun.Text.End < textRun.Text.End)
- {
- return currentDistance;
+ break;
}
- }
- else
- {
- if (characterIndex > textRun.Text.End)
+ default:
{
- return currentDistance;
+ if (characterIndex == currentPosition)
+ {
+ return currentDistance;
+ }
+
+ if (characterIndex == currentPosition + textRun.TextSourceLength)
+ {
+ return currentDistance + textRun.Size.Width;
+ }
+
+ break;
}
- }
}
//No hit hit found so we add the full width
- currentDistance += currentRun.Size.Width;
-
- lastRun = currentRun;
+ currentDistance += textRun.Size.Width;
+ currentPosition += textRun.TextSourceLength;
}
return currentDistance;
@@ -251,19 +336,38 @@ namespace Avalonia.Media.TextFormatting
///
public override CharacterHit GetNextCaretCharacterHit(CharacterHit characterHit)
{
+ if (_textRuns.Count == 0)
+ {
+ return new CharacterHit();
+ }
+
if (TryFindNextCharacterHit(characterHit, out var nextCharacterHit))
{
return nextCharacterHit;
}
+ var lastTextPosition = FirstTextSourceIndex + Length;
+
// Can't move, we're after the last character
- var runIndex = GetRunIndexAtCharacterIndex(TextRange.End, LogicalDirection.Forward);
+ var runIndex = GetRunIndexAtCharacterIndex(lastTextPosition, LogicalDirection.Forward, out var currentPosition);
- var textRun = _textRuns[runIndex];
+ var currentRun = _textRuns[runIndex];
- characterHit = textRun.GlyphRun.GetNextCaretCharacterHit(characterHit);
+ switch (currentRun)
+ {
+ case ShapedTextCharacters shapedRun:
+ {
+ characterHit = shapedRun.GlyphRun.GetNextCaretCharacterHit(characterHit);
+ break;
+ }
+ default:
+ {
+ characterHit = new CharacterHit(currentPosition + currentRun.TextSourceLength);
+ break;
+ }
+ }
- return characterHit;
+ return characterHit;
}
///
@@ -274,9 +378,9 @@ namespace Avalonia.Media.TextFormatting
return previousCharacterHit;
}
- if (characterHit.FirstCharacterIndex <= TextRange.Start)
+ if (characterHit.FirstCharacterIndex <= FirstTextSourceIndex)
{
- characterHit = new CharacterHit(TextRange.Start);
+ characterHit = new CharacterHit(FirstTextSourceIndex);
}
return characterHit; // Can't move, we're before the first character
@@ -289,9 +393,199 @@ namespace Avalonia.Media.TextFormatting
return GetPreviousCaretCharacterHit(characterHit);
}
- public static void SortRuns(List textRuns)
+ public override IReadOnlyList GetTextBounds(int firstTextSourceCharacterIndex, int textLength)
{
- textRuns.Sort(s_compareLogicalOrder);
+ if (firstTextSourceCharacterIndex + textLength <= FirstTextSourceIndex)
+ {
+ return Array.Empty();
+ }
+
+ var result = new List(TextRuns.Count);
+ var lastDirection = _flowDirection;
+ var currentDirection = lastDirection;
+ var currentPosition = 0;
+ var currentRect = Rect.Empty;
+ var startX = Start;
+
+ //A portion of the line is covered.
+ for (var index = 0; index < TextRuns.Count; index++)
+ {
+ var currentRun = TextRuns[index] as DrawableTextRun;
+
+ if (currentRun is null)
+ {
+ continue;
+ }
+
+ TextRun? nextRun = null;
+
+ if (index + 1 < TextRuns.Count)
+ {
+ nextRun = TextRuns[index + 1];
+ }
+
+ if (nextRun != null)
+ {
+ 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 > nextRun.Text.Start && currentRun.Text.Start < firstTextSourceCharacterIndex)
+ {
+ goto skip;
+ }
+
+ if (currentRun.Text.End < firstTextSourceCharacterIndex)
+ {
+ goto skip;
+ }
+
+ goto noop;
+
+ skip:
+ {
+ startX += currentRun.Size.Width;
+ }
+
+ continue;
+
+ noop:
+ {
+ }
+ }
+
+
+ var endX = startX;
+ var endOffset = 0d;
+
+ switch (currentRun)
+ {
+ case ShapedTextCharacters shapedRun:
+ {
+ endOffset = shapedRun.GlyphRun.GetDistanceFromCharacterHit(
+ shapedRun.ShapedBuffer.IsLeftToRight ?
+ new CharacterHit(firstTextSourceCharacterIndex + textLength) :
+ new CharacterHit(firstTextSourceCharacterIndex));
+
+ endX += endOffset;
+
+ var startOffset = shapedRun.GlyphRun.GetDistanceFromCharacterHit(
+ shapedRun.ShapedBuffer.IsLeftToRight ?
+ new CharacterHit(firstTextSourceCharacterIndex) :
+ new CharacterHit(firstTextSourceCharacterIndex + textLength));
+
+ startX += startOffset;
+
+ var characterHit = shapedRun.GlyphRun.IsLeftToRight ?
+ shapedRun.GlyphRun.GetCharacterHitFromDistance(endOffset, out _) :
+ shapedRun.GlyphRun.GetCharacterHitFromDistance(startOffset, out _);
+
+ currentPosition = characterHit.FirstCharacterIndex + characterHit.TrailingLength;
+
+ currentDirection = shapedRun.ShapedBuffer.IsLeftToRight ?
+ FlowDirection.LeftToRight :
+ FlowDirection.RightToLeft;
+
+ if (nextRun is ShapedTextCharacters nextShaped)
+ {
+ if (shapedRun.ShapedBuffer.IsLeftToRight == nextShaped.ShapedBuffer.IsLeftToRight)
+ {
+ endOffset = nextShaped.GlyphRun.GetDistanceFromCharacterHit(
+ nextShaped.ShapedBuffer.IsLeftToRight ?
+ new CharacterHit(firstTextSourceCharacterIndex + textLength) :
+ new CharacterHit(firstTextSourceCharacterIndex));
+
+ index++;
+
+ endX += endOffset;
+
+ currentRun = nextShaped;
+
+ if (nextShaped.ShapedBuffer.IsLeftToRight)
+ {
+ characterHit = nextShaped.GlyphRun.GetCharacterHitFromDistance(endOffset, out _);
+
+ currentPosition = characterHit.FirstCharacterIndex + characterHit.TrailingLength;
+ }
+ }
+ }
+
+ break;
+ }
+ default:
+ {
+ if (firstTextSourceCharacterIndex + textLength >= currentRun.Text.Start + currentRun.Text.Length)
+ {
+ endX += currentRun.Size.Width;
+ }
+
+ break;
+ }
+ }
+
+ if (endX < startX)
+ {
+ (endX, startX) = (startX, endX);
+ }
+
+ var width = endX - startX;
+
+ if (lastDirection == currentDirection && result.Count > 0 && MathUtilities.AreClose(currentRect.Right, startX))
+ {
+ var textBounds = new TextBounds(currentRect.WithWidth(currentRect.Width + width), currentDirection);
+
+ result[result.Count - 1] = textBounds;
+ }
+ else
+ {
+ currentRect = new Rect(startX, 0, width, Height);
+
+ result.Add(new TextBounds(currentRect, currentDirection));
+ }
+
+ 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)
+ {
+ break;
+ }
+ }
+ }
+ else
+ {
+ if (currentPosition <= firstTextSourceCharacterIndex)
+ {
+ break;
+ }
+
+ if (currentPosition != currentRun.Text.Start)
+ {
+ endX += currentRun.Size.Width - endOffset;
+ }
+ }
+
+ lastDirection = currentDirection;
+ startX = endX;
+ }
+
+ return result;
}
public TextLineImpl FinalizeLine()
@@ -303,18 +597,37 @@ namespace Avalonia.Media.TextFormatting
return this;
}
+ private static sbyte GetRunBidiLevel(DrawableTextRun run, FlowDirection flowDirection)
+ {
+ if (run is ShapedTextCharacters shapedTextCharacters)
+ {
+ return shapedTextCharacters.BidiLevel;
+ }
+
+ var defaultLevel = flowDirection == FlowDirection.LeftToRight ? 0 : 1;
+
+ return (sbyte)defaultLevel;
+ }
+
private void BidiReorder()
{
+ if (_textRuns.Count == 0)
+ {
+ return;
+ }
+
// Build up the collection of ordered runs.
var run = _textRuns[0];
- OrderedBidiRun orderedRun = new(run);
+
+ OrderedBidiRun orderedRun = new(run, GetRunBidiLevel(run, _flowDirection));
+
var current = orderedRun;
for (var i = 1; i < _textRuns.Count; i++)
{
run = _textRuns[i];
- current.Next = new OrderedBidiRun(run);
+ current.Next = new OrderedBidiRun(run, GetRunBidiLevel(run, _flowDirection));
current = current.Next;
}
@@ -331,7 +644,9 @@ namespace Avalonia.Media.TextFormatting
for (var i = 0; i < _textRuns.Count; i++)
{
- var level = _textRuns[i].BidiLevel;
+ var currentRun = _textRuns[i];
+
+ var level = GetRunBidiLevel(currentRun, _flowDirection);
if (level > max)
{
@@ -366,9 +681,9 @@ namespace Avalonia.Media.TextFormatting
{
if (current.Level >= minLevelToReverse && current.Level % 2 != 0)
{
- if (!current.Run.IsReversed)
+ if (current.Run is ShapedTextCharacters { IsReversed: false } shapedTextCharacters)
{
- current.Run.Reverse();
+ shapedTextCharacters.Reverse();
}
}
@@ -464,53 +779,73 @@ namespace Avalonia.Media.TextFormatting
nextCharacterHit = characterHit;
var codepointIndex = characterHit.FirstCharacterIndex + characterHit.TrailingLength;
+ var lastCodepointIndex = FirstTextSourceIndex + Length;
- if (codepointIndex >= TextRange.End)
+ if (codepointIndex >= lastCodepointIndex)
{
return false; // Cannot go forward anymore
}
- if (codepointIndex < TextRange.Start)
+ if (codepointIndex < FirstTextSourceIndex)
{
- codepointIndex = TextRange.Start;
+ codepointIndex = FirstTextSourceIndex;
}
- var runIndex = GetRunIndexAtCharacterIndex(codepointIndex, LogicalDirection.Forward);
+ var runIndex = GetRunIndexAtCharacterIndex(codepointIndex, LogicalDirection.Forward, out var currentPosition);
while (runIndex < _textRuns.Count)
{
- var run = _textRuns[runIndex];
+ var currentRun = _textRuns[runIndex];
+
+ switch (currentRun)
+ {
+ case ShapedTextCharacters shapedRun:
+ {
+ var foundCharacterHit = shapedRun.GlyphRun.FindNearestCharacterHit(characterHit.FirstCharacterIndex + characterHit.TrailingLength, out _);
- var foundCharacterHit =
- run.GlyphRun.FindNearestCharacterHit(characterHit.FirstCharacterIndex + characterHit.TrailingLength,
- out _);
+ var isAtEnd = foundCharacterHit.FirstCharacterIndex + foundCharacterHit.TrailingLength == FirstTextSourceIndex + Length;
- var isAtEnd = foundCharacterHit.FirstCharacterIndex + foundCharacterHit.TrailingLength ==
- TextRange.Start + TextRange.Length;
+ if (isAtEnd && !shapedRun.GlyphRun.IsLeftToRight)
+ {
+ nextCharacterHit = foundCharacterHit;
- if (isAtEnd && !run.GlyphRun.IsLeftToRight)
- {
- nextCharacterHit = foundCharacterHit;
+ return true;
+ }
- return true;
- }
+ var characterIndex = codepointIndex - shapedRun.Text.Start;
- var characterIndex = codepointIndex - run.Text.Start;
+ if (characterIndex < 0 && shapedRun.ShapedBuffer.IsLeftToRight)
+ {
+ foundCharacterHit = new CharacterHit(foundCharacterHit.FirstCharacterIndex);
+ }
- if (characterIndex < 0 && run.ShapedBuffer.IsLeftToRight)
- {
- foundCharacterHit = new CharacterHit(foundCharacterHit.FirstCharacterIndex);
- }
+ nextCharacterHit = isAtEnd || characterHit.TrailingLength != 0 ?
+ foundCharacterHit :
+ new CharacterHit(foundCharacterHit.FirstCharacterIndex + foundCharacterHit.TrailingLength);
+
+ if (isAtEnd || nextCharacterHit.FirstCharacterIndex > characterHit.FirstCharacterIndex)
+ {
+ return true;
+ }
- nextCharacterHit = isAtEnd || characterHit.TrailingLength != 0 ?
- foundCharacterHit :
- new CharacterHit(foundCharacterHit.FirstCharacterIndex + foundCharacterHit.TrailingLength);
+ break;
+ }
+ default:
+ {
+ var textPosition = characterHit.FirstCharacterIndex + characterHit.TrailingLength;
- if (isAtEnd || nextCharacterHit.FirstCharacterIndex > characterHit.FirstCharacterIndex)
- {
- return true;
+ if (textPosition == currentPosition)
+ {
+ nextCharacterHit = new CharacterHit(currentPosition + currentRun.TextSourceLength);
+
+ return true;
+ }
+
+ break;
+ }
}
+ currentPosition += currentRun.TextSourceLength;
runIndex++;
}
@@ -527,45 +862,67 @@ namespace Avalonia.Media.TextFormatting
{
var characterIndex = characterHit.FirstCharacterIndex + characterHit.TrailingLength;
- if (characterIndex == TextRange.Start)
+ if (characterIndex == FirstTextSourceIndex)
{
- previousCharacterHit = new CharacterHit(TextRange.Start);
+ previousCharacterHit = new CharacterHit(FirstTextSourceIndex);
return true;
}
previousCharacterHit = characterHit;
- if (characterIndex < TextRange.Start)
+ if (characterIndex < FirstTextSourceIndex)
{
return false; // Cannot go backward anymore.
}
- var runIndex = GetRunIndexAtCharacterIndex(characterIndex, LogicalDirection.Backward);
+ var runIndex = GetRunIndexAtCharacterIndex(characterIndex, LogicalDirection.Backward, out var currentPosition);
while (runIndex >= 0)
{
- var run = _textRuns[runIndex];
-
- var foundCharacterHit =
- run.GlyphRun.FindNearestCharacterHit(characterHit.FirstCharacterIndex - 1, out _);
+ var currentRun = _textRuns[runIndex];
- if (foundCharacterHit.FirstCharacterIndex + foundCharacterHit.TrailingLength < characterIndex)
+ switch (currentRun)
{
- previousCharacterHit = foundCharacterHit;
-
- return true;
- }
-
- previousCharacterHit = characterHit.TrailingLength != 0 ?
- foundCharacterHit :
- new CharacterHit(foundCharacterHit.FirstCharacterIndex);
+ case ShapedTextCharacters shapedRun:
+ {
+ var foundCharacterHit = shapedRun.GlyphRun.FindNearestCharacterHit(characterHit.FirstCharacterIndex - 1, out _);
- if (previousCharacterHit != characterHit)
- {
- return true;
+ if (foundCharacterHit.FirstCharacterIndex + foundCharacterHit.TrailingLength < characterIndex)
+ {
+ previousCharacterHit = foundCharacterHit;
+
+ return true;
+ }
+
+ var previousPosition = foundCharacterHit.FirstCharacterIndex + foundCharacterHit.TrailingLength;
+
+ if (foundCharacterHit.TrailingLength > 0 && previousPosition == characterIndex)
+ {
+ previousCharacterHit = new CharacterHit(foundCharacterHit.FirstCharacterIndex);
+ }
+
+ if (previousCharacterHit != characterHit)
+ {
+ return true;
+ }
+
+ break;
+ }
+ default:
+ {
+ if (characterIndex == currentPosition + currentRun.TextSourceLength)
+ {
+ previousCharacterHit = new CharacterHit(currentPosition);
+
+ return true;
+ }
+
+ break;
+ }
}
+ currentPosition -= currentRun.TextSourceLength;
runIndex--;
}
@@ -577,59 +934,83 @@ namespace Avalonia.Media.TextFormatting
///
/// The codepoint index.
/// The logical direction.
+ /// The text position of the found run index.
/// The text run index.
- private int GetRunIndexAtCharacterIndex(int codepointIndex, LogicalDirection direction)
+ private int GetRunIndexAtCharacterIndex(int codepointIndex, LogicalDirection direction, out int textPosition)
{
var runIndex = 0;
- ShapedTextCharacters? previousRun = null;
+ textPosition = FirstTextSourceIndex;
+ DrawableTextRun? previousRun = null;
while (runIndex < _textRuns.Count)
{
var currentRun = _textRuns[runIndex];
- if (previousRun != null && !previousRun.ShapedBuffer.IsLeftToRight)
+ switch (currentRun)
{
- if (currentRun.ShapedBuffer.IsLeftToRight)
- {
- if (currentRun.Text.Start >= codepointIndex)
+ case ShapedTextCharacters shapedRun:
{
- return --runIndex;
+ if (previousRun is ShapedTextCharacters previousShaped && !previousShaped.ShapedBuffer.IsLeftToRight)
+ {
+ if (shapedRun.ShapedBuffer.IsLeftToRight)
+ {
+ if (currentRun.Text.Start >= codepointIndex)
+ {
+ return --runIndex;
+ }
+ }
+ else
+ {
+ if (codepointIndex > currentRun.Text.Start + currentRun.Text.Length)
+ {
+ return --runIndex;
+ }
+ }
+ }
+
+ if (direction == LogicalDirection.Forward)
+ {
+ if (codepointIndex >= currentRun.Text.Start && codepointIndex <= currentRun.Text.End)
+ {
+ return runIndex;
+ }
+ }
+ else
+ {
+ if (codepointIndex > currentRun.Text.Start &&
+ codepointIndex <= currentRun.Text.Start + currentRun.Text.Length)
+ {
+ return runIndex;
+ }
+ }
+
+ if (runIndex + 1 >= _textRuns.Count)
+ {
+ return runIndex;
+ }
+
+ break;
}
- }
- else
- {
- if (codepointIndex > currentRun.Text.Start + currentRun.Text.Length)
+
+ default:
{
- return --runIndex;
- }
- }
- }
+ if (codepointIndex == textPosition)
+ {
+ return runIndex;
+ }
- if (direction == LogicalDirection.Forward)
- {
- if (codepointIndex >= currentRun.Text.Start && codepointIndex <= currentRun.Text.End)
- {
- return runIndex;
- }
- }
- else
- {
- if (codepointIndex > currentRun.Text.Start &&
- codepointIndex <= currentRun.Text.Start + currentRun.Text.Length)
- {
- return runIndex;
- }
- }
+ if (runIndex + 1 >= _textRuns.Count)
+ {
+ return runIndex;
+ }
- if (runIndex + 1 < _textRuns.Count)
- {
- runIndex++;
- previousRun = currentRun;
- }
- else
- {
- break;
+ break;
+ }
}
+
+ runIndex++;
+ previousRun = currentRun;
+ textPosition += currentRun.TextSourceLength;
}
return runIndex;
@@ -637,6 +1018,8 @@ namespace Avalonia.Media.TextFormatting
private TextLineMetrics CreateLineMetrics()
{
+ var start = 0d;
+ var height = 0d;
var width = 0d;
var widthIncludingWhitespace = 0d;
var trailingWhitespaceLength = 0;
@@ -646,78 +1029,150 @@ namespace Avalonia.Media.TextFormatting
var lineGap = 0d;
var fontRenderingEmSize = 0d;
- for (var index = 0; index < _textRuns.Count; index++)
+ var lineHeight = _paragraphProperties.LineHeight;
+
+ if (_textRuns.Count == 0)
{
- var textRun = _textRuns[index];
+ 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;
- var fontMetrics =
- new FontMetrics(textRun.Properties.Typeface, textRun.Properties.FontRenderingEmSize);
+ return new TextLineMetrics(false, height, 0, start, -ascent, 0, 0, 0);
+ }
- if (fontRenderingEmSize < textRun.Properties.FontRenderingEmSize)
+ for (var index = 0; index < _textRuns.Count; index++)
+ {
+ switch (_textRuns[index])
{
- fontRenderingEmSize = textRun.Properties.FontRenderingEmSize;
-
- if (ascent > fontMetrics.Ascent)
- {
- ascent = fontMetrics.Ascent;
- }
-
- if (descent < fontMetrics.Descent)
- {
- descent = fontMetrics.Descent;
- }
+ case ShapedTextCharacters textRun:
+ {
+ var fontMetrics =
+ new FontMetrics(textRun.Properties.Typeface, textRun.Properties.FontRenderingEmSize);
- if (lineGap < fontMetrics.LineGap)
- {
- lineGap = fontMetrics.LineGap;
- }
- }
+ if (fontRenderingEmSize < textRun.Properties.FontRenderingEmSize)
+ {
+ fontRenderingEmSize = textRun.Properties.FontRenderingEmSize;
+
+ if (ascent > fontMetrics.Ascent)
+ {
+ ascent = fontMetrics.Ascent;
+ }
+
+ if (descent < fontMetrics.Descent)
+ {
+ descent = fontMetrics.Descent;
+ }
+
+ if (lineGap < fontMetrics.LineGap)
+ {
+ lineGap = fontMetrics.LineGap;
+ }
+
+ if (descent - ascent + lineGap > height)
+ {
+ height = descent - ascent + lineGap;
+ }
+ }
- switch (_paragraphProperties.FlowDirection)
- {
- case FlowDirection.LeftToRight:
- {
- if (index == _textRuns.Count - 1)
+ switch (_paragraphProperties.FlowDirection)
{
- width = widthIncludingWhitespace + textRun.GlyphRun.Metrics.Width;
- trailingWhitespaceLength = textRun.GlyphRun.Metrics.TrailingWhitespaceLength;
- newLineLength = textRun.GlyphRun.Metrics.NewlineLength;
+ case FlowDirection.LeftToRight:
+ {
+ if (index == _textRuns.Count - 1)
+ {
+ width = widthIncludingWhitespace + textRun.GlyphRun.Metrics.Width;
+ trailingWhitespaceLength = textRun.GlyphRun.Metrics.TrailingWhitespaceLength;
+ newLineLength = textRun.GlyphRun.Metrics.NewlineLength;
+ }
+
+ break;
+ }
+
+ case FlowDirection.RightToLeft:
+ {
+ if (index == _textRuns.Count - 1)
+ {
+ var firstRun = _textRuns[0];
+
+ if (firstRun is ShapedTextCharacters shapedTextCharacters)
+ {
+ var offset = shapedTextCharacters.GlyphRun.Metrics.WidthIncludingTrailingWhitespace -
+ shapedTextCharacters.GlyphRun.Metrics.Width;
+
+ width = widthIncludingWhitespace +
+ textRun.GlyphRun.Metrics.WidthIncludingTrailingWhitespace - offset;
+
+ trailingWhitespaceLength = shapedTextCharacters.GlyphRun.Metrics.TrailingWhitespaceLength;
+ newLineLength = shapedTextCharacters.GlyphRun.Metrics.NewlineLength;
+ }
+ }
+
+ break;
+ }
}
+ widthIncludingWhitespace += textRun.GlyphRun.Metrics.WidthIncludingTrailingWhitespace;
+
break;
}
- case FlowDirection.RightToLeft:
+ case { } drawableTextRun:
{
- if (index == _textRuns.Count - 1)
- {
- var firstRun = _textRuns[0];
+ widthIncludingWhitespace += drawableTextRun.Size.Width;
- var offset = firstRun.GlyphRun.Metrics.WidthIncludingTrailingWhitespace -
- firstRun.GlyphRun.Metrics.Width;
+ switch (_paragraphProperties.FlowDirection)
+ {
+ case FlowDirection.LeftToRight:
+ {
+ if (index == _textRuns.Count - 1)
+ {
+ width = widthIncludingWhitespace;
+ trailingWhitespaceLength = 0;
+ newLineLength = 0;
+ }
+
+ break;
+ }
+
+ case FlowDirection.RightToLeft:
+ {
+ if (index == _textRuns.Count - 1)
+ {
+ width = widthIncludingWhitespace;
+ trailingWhitespaceLength = 0;
+ newLineLength = 0;
+ }
+
+ break;
+ }
+ }
- width = widthIncludingWhitespace +
- textRun.GlyphRun.Metrics.WidthIncludingTrailingWhitespace - offset;
+ if (drawableTextRun.Size.Height > height)
+ {
+ height = drawableTextRun.Size.Height;
+ }
- trailingWhitespaceLength = firstRun.GlyphRun.Metrics.TrailingWhitespaceLength;
- newLineLength = firstRun.GlyphRun.Metrics.NewlineLength;
+ if (ascent > -drawableTextRun.Baseline)
+ {
+ ascent = -drawableTextRun.Baseline;
}
break;
}
}
-
- widthIncludingWhitespace += textRun.GlyphRun.Metrics.WidthIncludingTrailingWhitespace;
}
- var start = GetParagraphOffsetX(width, widthIncludingWhitespace, _paragraphWidth,
+ start = GetParagraphOffsetX(width, widthIncludingWhitespace, _paragraphWidth,
_paragraphProperties.TextAlignment, _paragraphProperties.FlowDirection);
- var lineHeight = _paragraphProperties.LineHeight;
-
- var height = double.IsNaN(lineHeight) || MathUtilities.IsZero(lineHeight) ?
- descent - ascent + lineGap :
- lineHeight;
+ if (!double.IsNaN(lineHeight) && !MathUtilities.IsZero(lineHeight))
+ {
+ height = lineHeight;
+ }
return new TextLineMetrics(widthIncludingWhitespace > _paragraphWidth, height, newLineLength, start,
-ascent, trailingWhitespaceLength, width, widthIncludingWhitespace);
@@ -725,15 +1180,17 @@ namespace Avalonia.Media.TextFormatting
private sealed class OrderedBidiRun
{
- public OrderedBidiRun(ShapedTextCharacters run) => Run = run;
+ public OrderedBidiRun(DrawableTextRun run, sbyte level)
+ {
+ Run = run;
+ Level = level;
+ }
- public sbyte Level => Run.BidiLevel;
+ public sbyte Level { get; }
- public ShapedTextCharacters Run { get; }
+ public DrawableTextRun Run { get; }
public OrderedBidiRun? Next { get; set; }
-
- public void Reverse() => Run.ShapedBuffer.GlyphInfos.Span.Reverse();
}
private sealed class BidiRange
diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextParagraphProperties.cs b/src/Avalonia.Visuals/Media/TextFormatting/TextParagraphProperties.cs
index b799567a60..82a0ba14d8 100644
--- a/src/Avalonia.Visuals/Media/TextFormatting/TextParagraphProperties.cs
+++ b/src/Avalonia.Visuals/Media/TextFormatting/TextParagraphProperties.cs
@@ -63,5 +63,13 @@
{
get { return 0; }
}
+
+ ///
+ /// Default Incremental Tab
+ ///
+ public virtual double DefaultIncrementalTab
+ {
+ get { return 4 * DefaultTextRunProperties.FontRenderingEmSize; }
+ }
}
}
diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextRunProperties.cs b/src/Avalonia.Visuals/Media/TextFormatting/TextRunProperties.cs
index 99fcbd805f..86b701cb4b 100644
--- a/src/Avalonia.Visuals/Media/TextFormatting/TextRunProperties.cs
+++ b/src/Avalonia.Visuals/Media/TextFormatting/TextRunProperties.cs
@@ -45,7 +45,7 @@ namespace Avalonia.Media.TextFormatting
///
/// Run vertical box alignment
///
- public abstract BaselineAlignment BaselineAlignment { get; }
+ public virtual BaselineAlignment BaselineAlignment => BaselineAlignment.Baseline;
public bool Equals(TextRunProperties? other)
{
diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextShaper.cs b/src/Avalonia.Visuals/Media/TextFormatting/TextShaper.cs
index c982a435c3..615b1553b6 100644
--- a/src/Avalonia.Visuals/Media/TextFormatting/TextShaper.cs
+++ b/src/Avalonia.Visuals/Media/TextFormatting/TextShaper.cs
@@ -45,10 +45,9 @@ namespace Avalonia.Media.TextFormatting
}
///
- public ShapedBuffer ShapeText(ReadOnlySlice text, GlyphTypeface typeface, double fontRenderingEmSize,
- CultureInfo? culture, sbyte bidiLevel)
+ public ShapedBuffer ShapeText(ReadOnlySlice text, TextShaperOptions options)
{
- return _platformImpl.ShapeText(text, typeface, fontRenderingEmSize, culture, bidiLevel);
+ return _platformImpl.ShapeText(text, options);
}
}
}
diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextShaperOptions.cs b/src/Avalonia.Visuals/Media/TextFormatting/TextShaperOptions.cs
new file mode 100644
index 0000000000..a7fe92dc9a
--- /dev/null
+++ b/src/Avalonia.Visuals/Media/TextFormatting/TextShaperOptions.cs
@@ -0,0 +1,49 @@
+using System.Globalization;
+
+namespace Avalonia.Media.TextFormatting
+{
+ ///
+ /// Options to customize text shaping.
+ ///
+ public readonly struct TextShaperOptions
+ {
+ public TextShaperOptions(
+ GlyphTypeface typeface,
+ double fontRenderingEmSize = 12,
+ sbyte bidiLevel = 0,
+ CultureInfo? culture = null,
+ double incrementalTabWidth = 0)
+ {
+ Typeface = typeface;
+ FontRenderingEmSize = fontRenderingEmSize;
+ BidLevel = bidiLevel;
+ Culture = culture;
+ IncrementalTabWidth = incrementalTabWidth;
+ }
+
+ ///
+ /// Get the typeface.
+ ///
+ public GlyphTypeface Typeface { get; }
+ ///
+ /// Get the font rendering em size.
+ ///
+ public double FontRenderingEmSize { get; }
+
+ ///
+ /// Get the bidi level of the text.
+ ///
+ public sbyte BidLevel { get; }
+
+ ///
+ /// Get the culture.
+ ///
+ public CultureInfo? Culture { get; }
+
+ ///
+ /// Get the incremental tab width.
+ ///
+ public double IncrementalTabWidth { get; }
+
+ }
+}
diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextTrailingCharacterEllipsis.cs b/src/Avalonia.Visuals/Media/TextFormatting/TextTrailingCharacterEllipsis.cs
index 83acaa021e..670d94e928 100644
--- a/src/Avalonia.Visuals/Media/TextFormatting/TextTrailingCharacterEllipsis.cs
+++ b/src/Avalonia.Visuals/Media/TextFormatting/TextTrailingCharacterEllipsis.cs
@@ -14,7 +14,7 @@ namespace Avalonia.Media.TextFormatting
///
/// Text used as collapsing symbol.
/// Width in which collapsing is constrained to.
- /// Text run properties of ellispis symbol.
+ /// Text run properties of ellipsis symbol.
public TextTrailingCharacterEllipsis(ReadOnlySlice ellipsis, double width, TextRunProperties textRunProperties)
{
Width = width;
@@ -22,12 +22,12 @@ namespace Avalonia.Media.TextFormatting
}
///
- public sealed override double Width { get; }
+ public override double Width { get; }
///
- public sealed override TextRun Symbol { get; }
+ public override TextRun Symbol { get; }
- public override IReadOnlyList? Collapse(TextLine textLine)
+ public override List? Collapse(TextLine textLine)
{
return TextEllipsisHelper.Collapse(textLine, this, false);
}
diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextTrailingWordEllipsis.cs b/src/Avalonia.Visuals/Media/TextFormatting/TextTrailingWordEllipsis.cs
index ff2e4cf325..dbffbdf060 100644
--- a/src/Avalonia.Visuals/Media/TextFormatting/TextTrailingWordEllipsis.cs
+++ b/src/Avalonia.Visuals/Media/TextFormatting/TextTrailingWordEllipsis.cs
@@ -14,7 +14,7 @@ namespace Avalonia.Media.TextFormatting
///
/// Text used as collapsing symbol.
/// width in which collapsing is constrained to.
- /// text run properties of ellispis symbol.
+ /// text run properties of ellipsis symbol.
public TextTrailingWordEllipsis(
ReadOnlySlice ellipsis,
double width,
@@ -26,12 +26,12 @@ namespace Avalonia.Media.TextFormatting
}
///
- public sealed override double Width { get; }
+ public override double Width { get; }
///
- public sealed override TextRun Symbol { get; }
+ public override TextRun Symbol { get; }
- public override IReadOnlyList? Collapse(TextLine textLine)
+ public override List? Collapse(TextLine textLine)
{
return TextEllipsisHelper.Collapse(textLine, this, true);
}
diff --git a/src/Avalonia.Visuals/Media/TextFormatting/Unicode/BiDiAlgorithm.cs b/src/Avalonia.Visuals/Media/TextFormatting/Unicode/BiDiAlgorithm.cs
index 404956d1e1..d18a4b2a87 100644
--- a/src/Avalonia.Visuals/Media/TextFormatting/Unicode/BiDiAlgorithm.cs
+++ b/src/Avalonia.Visuals/Media/TextFormatting/Unicode/BiDiAlgorithm.cs
@@ -302,7 +302,7 @@ namespace Avalonia.Media.TextFormatting.Unicode
///
/// The data to be evaluated
/// The resolved embedding level
- public sbyte ResolveEmbeddingLevel(ReadOnlySlice data)
+ public sbyte ResolveEmbeddingLevel(ArraySlice data)
{
// P2
for (var i = 0; i < data.Length; ++i)
diff --git a/src/Avalonia.Visuals/Platform/IGeometryContext.cs b/src/Avalonia.Visuals/Platform/IGeometryContext.cs
index 3f7273b53b..87db9f1dd4 100644
--- a/src/Avalonia.Visuals/Platform/IGeometryContext.cs
+++ b/src/Avalonia.Visuals/Platform/IGeometryContext.cs
@@ -13,7 +13,7 @@ namespace Avalonia.Platform
///
/// The destination point.
/// The radii of an oval whose perimeter is used to draw the angle.
- /// The rotation angle of the oval that specifies the curve.
+ /// The rotation angle (in radians) of the oval that specifies the curve.
/// true to draw the arc greater than 180 degrees; otherwise, false.
///
/// A value that indicates whether the arc is drawn in the Clockwise or Counterclockwise direction.
@@ -60,4 +60,4 @@ namespace Avalonia.Platform
/// The fill rule.
void SetFillRule(FillRule fillRule);
}
-}
\ No newline at end of file
+}
diff --git a/src/Avalonia.Visuals/Platform/IPlatformRenderInterface.cs b/src/Avalonia.Visuals/Platform/IPlatformRenderInterface.cs
index e4a280938d..444cc2eb8c 100644
--- a/src/Avalonia.Visuals/Platform/IPlatformRenderInterface.cs
+++ b/src/Avalonia.Visuals/Platform/IPlatformRenderInterface.cs
@@ -168,6 +168,13 @@ namespace Avalonia.Platform
///
IGlyphRunImpl CreateGlyphRun(GlyphRun glyphRun);
+ ///
+ /// Gets a value indicating whether the platform directly supports rectangles with rounded corners.
+ ///
+ ///
+ /// Some platform renderers can't directly handle rounded corners on rectangles.
+ /// In this case, code that requires rounded corners must generate and retain a geometry instead.
+ ///
bool SupportsIndividualRoundRects { get; }
///
diff --git a/src/Avalonia.Visuals/Platform/ITextShaperImpl.cs b/src/Avalonia.Visuals/Platform/ITextShaperImpl.cs
index aced05c9d8..11be9e3f09 100644
--- a/src/Avalonia.Visuals/Platform/ITextShaperImpl.cs
+++ b/src/Avalonia.Visuals/Platform/ITextShaperImpl.cs
@@ -1,6 +1,4 @@
-using System.Globalization;
-using Avalonia.Media;
-using Avalonia.Media.TextFormatting;
+using Avalonia.Media.TextFormatting;
using Avalonia.Utilities;
namespace Avalonia.Platform
@@ -14,11 +12,8 @@ namespace Avalonia.Platform
/// Shapes the specified region within the text and returns a shaped buffer.
///
/// The text.
- /// The typeface.
- /// The font rendering em size.
- /// The culture.
- /// The bidi level.
+ /// Text shaper options to customize the shaping process.
/// A shaped glyph run.
- ShapedBuffer ShapeText(ReadOnlySlice text, GlyphTypeface typeface, double fontRenderingEmSize, CultureInfo? culture, sbyte bidiLevel);
- }
+ ShapedBuffer ShapeText(ReadOnlySlice text, TextShaperOptions options);
+ }
}
diff --git a/src/Avalonia.Visuals/Rendering/ImmediateRenderer.cs b/src/Avalonia.Visuals/Rendering/ImmediateRenderer.cs
index 23016de148..2c0298affa 100644
--- a/src/Avalonia.Visuals/Rendering/ImmediateRenderer.cs
+++ b/src/Avalonia.Visuals/Rendering/ImmediateRenderer.cs
@@ -285,6 +285,12 @@ namespace Avalonia.Rendering
renderTransform = (-offset) * visual.RenderTransform.Value * (offset);
}
+ if (visual.HasMirrorTransform)
+ {
+ var mirrorMatrix = new Matrix(-1.0, 0.0, 0.0, 1.0, visual.Bounds.Width, 0);
+ renderTransform *= mirrorMatrix;
+ }
+
m = renderTransform * m;
if (clipToBounds)
diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/EllipseNode.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/EllipseNode.cs
index 504256b932..c1fc6a81f6 100644
--- a/src/Avalonia.Visuals/Rendering/SceneGraph/EllipseNode.cs
+++ b/src/Avalonia.Visuals/Rendering/SceneGraph/EllipseNode.cs
@@ -59,6 +59,7 @@ namespace Avalonia.Rendering.SceneGraph
public override void Render(IDrawingContextImpl context)
{
+ context.Transform = Transform;
context.DrawEllipse(Brush, Pen, Rect);
}
diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/SceneBuilder.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/SceneBuilder.cs
index 63c22efc3f..019c3e0e9b 100644
--- a/src/Avalonia.Visuals/Rendering/SceneGraph/SceneBuilder.cs
+++ b/src/Avalonia.Visuals/Rendering/SceneGraph/SceneBuilder.cs
@@ -195,6 +195,12 @@ namespace Avalonia.Rendering.SceneGraph
renderTransform = (-offset) * visual.RenderTransform.Value * (offset);
}
+ if (visual.HasMirrorTransform)
+ {
+ var mirrorMatrix = new Matrix(-1.0, 0.0, 0.0, 1.0, visual.Bounds.Width, 0);
+ renderTransform *= mirrorMatrix;
+ }
+
m = renderTransform * m;
using (contextImpl.BeginUpdate(node))
diff --git a/src/Avalonia.Visuals/Visual.cs b/src/Avalonia.Visuals/Visual.cs
index 324b253a0f..26a7d5d3b8 100644
--- a/src/Avalonia.Visuals/Visual.cs
+++ b/src/Avalonia.Visuals/Visual.cs
@@ -68,6 +68,12 @@ namespace Avalonia
public static readonly StyledProperty OpacityMaskProperty =
AvaloniaProperty.Register(nameof(OpacityMask));
+ ///
+ /// Defines the property.
+ ///
+ public static readonly DirectProperty HasMirrorTransformProperty =
+ AvaloniaProperty.RegisterDirect(nameof(HasMirrorTransform), o => o.HasMirrorTransform);
+
///
/// Defines the property.
///
@@ -96,6 +102,7 @@ namespace Avalonia
private TransformedBounds? _transformedBounds;
private IRenderRoot? _visualRoot;
private IVisual? _visualParent;
+ private bool _hasMirrorTransform;
///
/// Initializes static members of the class.
@@ -107,7 +114,8 @@ namespace Avalonia
ClipProperty,
ClipToBoundsProperty,
IsVisibleProperty,
- OpacityProperty);
+ OpacityProperty,
+ HasMirrorTransformProperty);
RenderTransformProperty.Changed.Subscribe(RenderTransformChanged);
ZIndexProperty.Changed.Subscribe(ZIndexChanged);
}
@@ -219,6 +227,15 @@ namespace Avalonia
set { SetValue(OpacityMaskProperty, value); }
}
+ ///
+ /// Gets or sets a value indicating whether to apply mirror transform on this control.
+ ///
+ public bool HasMirrorTransform
+ {
+ get { return _hasMirrorTransform; }
+ protected set { SetAndRaise(HasMirrorTransformProperty, ref _hasMirrorTransform, value); }
+ }
+
///
/// Gets or sets the render transform of the control.
///
diff --git a/src/Avalonia.Visuals/VisualExtensions.cs b/src/Avalonia.Visuals/VisualExtensions.cs
index ff8a515db3..3a3c2693d0 100644
--- a/src/Avalonia.Visuals/VisualExtensions.cs
+++ b/src/Avalonia.Visuals/VisualExtensions.cs
@@ -110,6 +110,12 @@ namespace Avalonia
result *= renderTransform;
}
+ if (v.HasMirrorTransform)
+ {
+ var mirrorMatrix = new Matrix(-1.0, 0.0, 0.0, 1.0, v.Bounds.Width, 0);
+ result *= mirrorMatrix;
+ }
+
var topLeft = v.Bounds.TopLeft;
if (topLeft != default)
diff --git a/src/Avalonia.Visuals/VisualTree/IVisual.cs b/src/Avalonia.Visuals/VisualTree/IVisual.cs
index 97c4554de6..b1251618c4 100644
--- a/src/Avalonia.Visuals/VisualTree/IVisual.cs
+++ b/src/Avalonia.Visuals/VisualTree/IVisual.cs
@@ -75,6 +75,11 @@ namespace Avalonia.VisualTree
///
IBrush? OpacityMask { get; set; }
+ ///
+ /// Gets a value indicating whether to apply mirror transform on this control.
+ ///
+ bool HasMirrorTransform { get; }
+
///
/// Gets or sets the render transform of the control.
///
diff --git a/src/Avalonia.X11/Glx/GlxDisplay.cs b/src/Avalonia.X11/Glx/GlxDisplay.cs
index fcdc10e999..1e70608168 100644
--- a/src/Avalonia.X11/Glx/GlxDisplay.cs
+++ b/src/Avalonia.X11/Glx/GlxDisplay.cs
@@ -9,7 +9,7 @@ namespace Avalonia.X11.Glx
unsafe class GlxDisplay
{
private readonly X11Info _x11;
- private readonly List _probeProfiles;
+ private readonly GlVersion[] _probeProfiles;
private readonly IntPtr _fbconfig;
private readonly XVisualInfo* _visual;
private string[] _displayExtensions;
@@ -21,7 +21,7 @@ namespace Avalonia.X11.Glx
public GlxDisplay(X11Info x11, IList probeProfiles)
{
_x11 = x11;
- _probeProfiles = probeProfiles.ToList();
+ _probeProfiles = probeProfiles.ToArray();
_displayExtensions = Glx.GetExtensions(_x11.Display);
var baseAttribs = new[]
@@ -76,10 +76,10 @@ namespace Avalonia.X11.Glx
if (Glx.GetFBConfigAttrib(_x11.Display, _fbconfig, GLX_STENCIL_SIZE, out var stencil) == 0)
stencilSize = stencil;
- var pbuffers = Enumerable.Range(0, 2).Select(_ => Glx.CreatePbuffer(_x11.Display, _fbconfig, new[]
- {
- GLX_PBUFFER_WIDTH, 1, GLX_PBUFFER_HEIGHT, 1, 0
- })).ToList();
+ var attributes = new[] { GLX_PBUFFER_WIDTH, 1, GLX_PBUFFER_HEIGHT, 1, 0 };
+
+ Glx.CreatePbuffer(_x11.Display, _fbconfig, attributes);
+ Glx.CreatePbuffer(_x11.Display, _fbconfig, attributes);
XLib.XFlush(_x11.Display);
@@ -104,7 +104,6 @@ namespace Avalonia.X11.Glx
$"Renderer '{glInterface.Renderer}' is blacklisted by '{item}'");
}
}
-
}
IntPtr CreatePBuffer()
diff --git a/src/Avalonia.X11/X11Globals.cs b/src/Avalonia.X11/X11Globals.cs
index 057693f810..39834a44b3 100644
--- a/src/Avalonia.X11/X11Globals.cs
+++ b/src/Avalonia.X11/X11Globals.cs
@@ -41,7 +41,7 @@ namespace Avalonia.X11
{
_wmName = value;
// The collection might change during enumeration
- foreach (var s in _subscribers.ToList())
+ foreach (var s in _subscribers.ToArray())
s.WmChanged(value);
}
}
@@ -69,7 +69,7 @@ namespace Avalonia.X11
{
_isCompositionEnabled = value;
// The collection might change during enumeration
- foreach (var s in _subscribers.ToList())
+ foreach (var s in _subscribers.ToArray())
s.CompositionChanged(value);
}
}
diff --git a/src/Linux/Avalonia.LinuxFramebuffer/Output/DrmBindings.cs b/src/Linux/Avalonia.LinuxFramebuffer/Output/DrmBindings.cs
index e218da5f41..64fafc65f3 100644
--- a/src/Linux/Avalonia.LinuxFramebuffer/Output/DrmBindings.cs
+++ b/src/Linux/Avalonia.LinuxFramebuffer/Output/DrmBindings.cs
@@ -1,7 +1,9 @@
using System;
+using System.IO;
using System.Collections.Generic;
using System.ComponentModel;
using System.Runtime.InteropServices;
+using System.Text.RegularExpressions;
using static Avalonia.LinuxFramebuffer.NativeUnsafeMethods;
using static Avalonia.LinuxFramebuffer.Output.LibDrm;
@@ -142,10 +144,28 @@ namespace Avalonia.LinuxFramebuffer.Output
public int Fd { get; private set; }
public DrmCard(string path = null)
{
- path = path ?? "/dev/dri/card0";
- Fd = open(path, 2, 0);
- if (Fd == -1)
- throw new Win32Exception("Couldn't open " + path);
+ if(path == null)
+ {
+ var files = Directory.GetFiles("/dev/dri/");
+
+ foreach(var file in files)
+ {
+ var match = Regex.Match(file, "card[0-9]+");
+
+ if(match.Success)
+ {
+ Fd = open(file, 2, 0);
+ if(Fd != -1) break;
+ }
+ }
+
+ if(Fd == -1) throw new Win32Exception("Couldn't open /dev/dri/card[0-9]+");
+ }
+ else
+ {
+ Fd = open(path, 2, 0);
+ if(Fd != -1) throw new Win32Exception($"Couldn't open {path}");
+ }
}
public DrmResources GetResources() => new DrmResources(Fd);
diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/Avalonia.Markup.Xaml.Loader.csproj b/src/Markup/Avalonia.Markup.Xaml.Loader/Avalonia.Markup.Xaml.Loader.csproj
index f75c96c759..b89ea8399a 100644
--- a/src/Markup/Avalonia.Markup.Xaml.Loader/Avalonia.Markup.Xaml.Loader.csproj
+++ b/src/Markup/Avalonia.Markup.Xaml.Loader/Avalonia.Markup.Xaml.Loader.csproj
@@ -12,5 +12,6 @@
-
+
+
diff --git a/src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj b/src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj
index 548aae31a8..6624392258 100644
--- a/src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj
+++ b/src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj
@@ -68,4 +68,5 @@
+
diff --git a/src/Markup/Avalonia.Markup/Avalonia.Markup.csproj b/src/Markup/Avalonia.Markup/Avalonia.Markup.csproj
index 21370b3bf5..119f57973c 100644
--- a/src/Markup/Avalonia.Markup/Avalonia.Markup.csproj
+++ b/src/Markup/Avalonia.Markup/Avalonia.Markup.csproj
@@ -18,4 +18,5 @@
+
diff --git a/src/Shared/ModuleInitializer.cs b/src/Shared/ModuleInitializer.cs
index c14b150c1a..b5bb31ed81 100644
--- a/src/Shared/ModuleInitializer.cs
+++ b/src/Shared/ModuleInitializer.cs
@@ -3,7 +3,7 @@ namespace System.Runtime.CompilerServices
#if !NET5_0_OR_GREATER
internal class ModuleInitializerAttribute : Attribute
{
-
+
}
#endif
}
diff --git a/src/Skia/Avalonia.Skia/Avalonia.Skia.csproj b/src/Skia/Avalonia.Skia/Avalonia.Skia.csproj
index 50ace8209c..26c0fb756e 100644
--- a/src/Skia/Avalonia.Skia/Avalonia.Skia.csproj
+++ b/src/Skia/Avalonia.Skia/Avalonia.Skia.csproj
@@ -16,5 +16,6 @@
-
+
+
diff --git a/src/Skia/Avalonia.Skia/FontManagerImpl.cs b/src/Skia/Avalonia.Skia/FontManagerImpl.cs
index 075a2cc746..125dd0e455 100644
--- a/src/Skia/Avalonia.Skia/FontManagerImpl.cs
+++ b/src/Skia/Avalonia.Skia/FontManagerImpl.cs
@@ -122,9 +122,7 @@ namespace Avalonia.Skia
skTypeface = _skFontManager.MatchFamily(familyName, fontStyle);
- if (skTypeface is null
- || (!skTypeface.FamilyName.Equals(familyName, StringComparison.Ordinal)
- && defaultName.Equals(skTypeface.FamilyName, StringComparison.Ordinal)))
+ if (skTypeface is null || defaultName.Equals(skTypeface.FamilyName, StringComparison.Ordinal))
{
continue;
}
diff --git a/src/Skia/Avalonia.Skia/SKTypefaceCollection.cs b/src/Skia/Avalonia.Skia/SKTypefaceCollection.cs
index f66df9e6e9..1f3f20730f 100644
--- a/src/Skia/Avalonia.Skia/SKTypefaceCollection.cs
+++ b/src/Skia/Avalonia.Skia/SKTypefaceCollection.cs
@@ -59,15 +59,30 @@ namespace Avalonia.Skia
return typeface;
}
- //Nothing was found so we try some regular cases.
+ if (TryFindStretchFallback(key, out typeface))
+ {
+ return typeface;
+ }
+
+ //Nothing was found so we try some regular typeface.
if (_typefaces.TryGetValue(new Typeface(key.FontFamily), out typeface))
{
return typeface;
}
- return _typefaces.TryGetValue(new Typeface(key.FontFamily, FontStyle.Italic), out typeface) ?
- typeface :
- null;
+ SKTypeface skTypeface = null;
+
+ foreach(var pair in _typefaces)
+ {
+ skTypeface = pair.Value;
+
+ if (skTypeface.FamilyName.Contains(key.FontFamily.Name))
+ {
+ return skTypeface;
+ }
+ }
+
+ return skTypeface;
}
private bool TryFindStretchFallback(Typeface key, out SKTypeface typeface)
diff --git a/src/Skia/Avalonia.Skia/TextShaperImpl.cs b/src/Skia/Avalonia.Skia/TextShaperImpl.cs
index c4d11f4613..a0890262e7 100644
--- a/src/Skia/Avalonia.Skia/TextShaperImpl.cs
+++ b/src/Skia/Avalonia.Skia/TextShaperImpl.cs
@@ -13,12 +13,16 @@ namespace Avalonia.Skia
{
internal class TextShaperImpl : ITextShaperImpl
{
- public ShapedBuffer ShapeText(ReadOnlySlice text, GlyphTypeface typeface, double fontRenderingEmSize,
- CultureInfo culture, sbyte bidiLevel)
+ public ShapedBuffer ShapeText(ReadOnlySlice text, TextShaperOptions options)
{
+ var typeface = options.Typeface;
+ var fontRenderingEmSize = options.FontRenderingEmSize;
+ var bidiLevel = options.BidLevel;
+ var culture = options.Culture;
+
using (var buffer = new Buffer())
{
- buffer.AddUtf16(text.Buffer.Span, text.Start, text.Length);
+ buffer.AddUtf16(text.Buffer.Span, text.BufferOffset, text.Length);
MergeBreakPair(buffer);
@@ -61,6 +65,15 @@ namespace Avalonia.Skia
var glyphOffset = GetGlyphOffset(glyphPositions, i, textScale);
+ if(glyphIndex == 0 && text.Buffer.Span[glyphCluster] == '\t')
+ {
+ glyphIndex = typeface.GetGlyph(' ');
+
+ glyphAdvance = options.IncrementalTabWidth > 0 ?
+ options.IncrementalTabWidth :
+ 4 * typeface.GetGlyphAdvance(glyphIndex) * textScale;
+ }
+
var targetInfo = new Media.TextFormatting.GlyphInfo(glyphIndex, glyphCluster, glyphAdvance, glyphOffset);
shapedBuffer[i] = targetInfo;
diff --git a/src/Windows/Avalonia.Direct2D1/Avalonia.Direct2D1.csproj b/src/Windows/Avalonia.Direct2D1/Avalonia.Direct2D1.csproj
index a1d8a60cb9..bd1f05b30f 100644
--- a/src/Windows/Avalonia.Direct2D1/Avalonia.Direct2D1.csproj
+++ b/src/Windows/Avalonia.Direct2D1/Avalonia.Direct2D1.csproj
@@ -18,4 +18,5 @@
+
diff --git a/src/Windows/Avalonia.Direct2D1/Media/TextShaperImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/TextShaperImpl.cs
index 62cf031f86..59027a663f 100644
--- a/src/Windows/Avalonia.Direct2D1/Media/TextShaperImpl.cs
+++ b/src/Windows/Avalonia.Direct2D1/Media/TextShaperImpl.cs
@@ -14,9 +14,13 @@ namespace Avalonia.Direct2D1.Media
internal class TextShaperImpl : ITextShaperImpl
{
- public ShapedBuffer ShapeText(ReadOnlySlice text, GlyphTypeface typeface, double fontRenderingEmSize,
- CultureInfo culture, sbyte bidiLevel)
+ public ShapedBuffer ShapeText(ReadOnlySlice text, TextShaperOptions options)
{
+ var typeface = options.Typeface;
+ var fontRenderingEmSize = options.FontRenderingEmSize;
+ var bidiLevel = options.BidLevel;
+ var culture = options.Culture;
+
using (var buffer = new Buffer())
{
buffer.AddUtf16(text.Buffer.Span, text.Start, text.Length);
@@ -62,6 +66,15 @@ internal class TextShaperImpl : ITextShaperImpl
var glyphOffset = GetGlyphOffset(glyphPositions, i, textScale);
+ if (glyphIndex == 0 && text[glyphCluster] == '\t')
+ {
+ glyphIndex = typeface.GetGlyph(' ');
+
+ glyphAdvance = options.IncrementalTabWidth > 0 ?
+ options.IncrementalTabWidth :
+ 4 * typeface.GetGlyphAdvance(glyphIndex) * textScale;
+ }
+
var targetInfo =
new Avalonia.Media.TextFormatting.GlyphInfo(glyphIndex, glyphCluster, glyphAdvance,
glyphOffset);
diff --git a/src/Windows/Avalonia.Win32.Interop/Avalonia.Win32.Interop.csproj b/src/Windows/Avalonia.Win32.Interop/Avalonia.Win32.Interop.csproj
index 9f49808422..cc8a40e2d4 100644
--- a/src/Windows/Avalonia.Win32.Interop/Avalonia.Win32.Interop.csproj
+++ b/src/Windows/Avalonia.Win32.Interop/Avalonia.Win32.Interop.csproj
@@ -18,4 +18,5 @@
+
diff --git a/src/Windows/Avalonia.Win32/Avalonia.Win32.csproj b/src/Windows/Avalonia.Win32/Avalonia.Win32.csproj
index ba842757d7..b9a1880435 100644
--- a/src/Windows/Avalonia.Win32/Avalonia.Win32.csproj
+++ b/src/Windows/Avalonia.Win32/Avalonia.Win32.csproj
@@ -16,5 +16,6 @@
-
+
+
diff --git a/src/Windows/Avalonia.Win32/TrayIconImpl.cs b/src/Windows/Avalonia.Win32/TrayIconImpl.cs
index 23395dd9b5..6484ae6c54 100644
--- a/src/Windows/Avalonia.Win32/TrayIconImpl.cs
+++ b/src/Windows/Avalonia.Win32/TrayIconImpl.cs
@@ -217,8 +217,9 @@ namespace Avalonia.Win32
}
public IReadOnlyList Screens =>
- _hiddenWindow.Screens.All.Select(s => new ManagedPopupPositionerScreenInfo(
- s.Bounds.ToRect(1), s.Bounds.ToRect(1))).ToList();
+ _hiddenWindow.Screens.All
+ .Select(s => new ManagedPopupPositionerScreenInfo(s.Bounds.ToRect(1), s.Bounds.ToRect(1)))
+ .ToArray();
public Rect ParentClientAreaScreenGeometry
{
diff --git a/src/Windows/Avalonia.Win32/Win32GlManager.cs b/src/Windows/Avalonia.Win32/Win32GlManager.cs
index 0376a41f8c..2a3f4a3384 100644
--- a/src/Windows/Avalonia.Win32/Win32GlManager.cs
+++ b/src/Windows/Avalonia.Win32/Win32GlManager.cs
@@ -24,9 +24,23 @@ namespace Avalonia.Win32
{
var egl = EglPlatformOpenGlInterface.TryCreate(() => new AngleWin32EglDisplay());
- if (egl != null && opts.UseWindowsUIComposition)
+ if (egl != null)
{
- WinUICompositorConnection.TryCreateAndRegister(egl, opts.CompositionBackdropCornerRadius);
+ if (opts.EglRendererBlacklist != null)
+ {
+ foreach (var item in opts.EglRendererBlacklist)
+ {
+ if (egl.PrimaryEglContext.GlInterface.Renderer.Contains(item))
+ {
+ return null;
+ }
+ }
+ }
+
+ if (opts.UseWindowsUIComposition)
+ {
+ WinUICompositorConnection.TryCreateAndRegister(egl, opts.CompositionBackdropCornerRadius);
+ }
}
return egl;
diff --git a/src/Windows/Avalonia.Win32/Win32Platform.cs b/src/Windows/Avalonia.Win32/Win32Platform.cs
index 25a6717122..aa597d6809 100644
--- a/src/Windows/Avalonia.Win32/Win32Platform.cs
+++ b/src/Windows/Avalonia.Win32/Win32Platform.cs
@@ -56,6 +56,11 @@ namespace Avalonia
/// GPU rendering will not be enabled if this is set to false.
///
public bool? AllowEglInitialization { get; set; }
+
+ public IList EglRendererBlacklist { get; set; } = new List
+ {
+ "Microsoft Basic Render"
+ };
///
/// Enables multitouch support. The default value is true.
diff --git a/src/iOS/Avalonia.iOS/Avalonia.iOS.csproj b/src/iOS/Avalonia.iOS/Avalonia.iOS.csproj
index 39a0305b1e..ea3a79941d 100644
--- a/src/iOS/Avalonia.iOS/Avalonia.iOS.csproj
+++ b/src/iOS/Avalonia.iOS/Avalonia.iOS.csproj
@@ -1,8 +1,6 @@
-
-
+
- net6.0-ios
- $(TargetFrameworks);xamarin.ios10
+ net6.0-ios10.0true
@@ -10,13 +8,6 @@
-
-
-
-
-
- <_BuiltProjectOutputGroupOutputIntermediate Remove="$(OutDir)$(_DeploymentTargetApplicationManifestFileName)" />
-
-
-
+
+
diff --git a/src/tools/DevAnalyzers/DevAnalyzers.csproj b/src/tools/DevAnalyzers/DevAnalyzers.csproj
new file mode 100644
index 0000000000..53e3e74e76
--- /dev/null
+++ b/src/tools/DevAnalyzers/DevAnalyzers.csproj
@@ -0,0 +1,17 @@
+
+
+
+ netstandard2.0
+ 10
+ enable
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
+
+
diff --git a/src/tools/DevAnalyzers/GenericVirtualAnalyzer.cs b/src/tools/DevAnalyzers/GenericVirtualAnalyzer.cs
new file mode 100644
index 0000000000..8ecd9119f6
--- /dev/null
+++ b/src/tools/DevAnalyzers/GenericVirtualAnalyzer.cs
@@ -0,0 +1,40 @@
+using System.Collections.Immutable;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.Diagnostics;
+
+namespace DevAnalyzers;
+
+[DiagnosticAnalyzer(LanguageNames.CSharp)]
+public class GenericVirtualAnalyzer : DiagnosticAnalyzer
+{
+ public const string DiagnosticId = "AVADEV1001";
+
+ private static readonly DiagnosticDescriptor Rule = new DiagnosticDescriptor(
+ DiagnosticId,
+ "Do not use generic virtual methods",
+ "Method '{0}' is a generic virtual method",
+ "Performance",
+ DiagnosticSeverity.Warning,
+ isEnabledByDefault: true,
+ description: "Generic virtual methods affect JIT startup time adversly and should be avoided.");
+
+ public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create(Rule);
+
+ public override void Initialize(AnalysisContext context)
+ {
+ context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
+ context.EnableConcurrentExecution();
+ context.RegisterSymbolAction(AnalyzeMethod, SymbolKind.Method);
+ }
+
+ private static void AnalyzeMethod(SymbolAnalysisContext context)
+ {
+ var symbol = (IMethodSymbol)context.Symbol;
+
+ if (symbol.IsGenericMethod &&
+ (symbol.IsVirtual || symbol.ContainingType.TypeKind == TypeKind.Interface))
+ {
+ context.ReportDiagnostic(Diagnostic.Create(Rule, symbol.Locations[0], symbol.Name));
+ }
+ }
+}
diff --git a/src/tools/DevAnalyzers/GlobalSuppressions.cs b/src/tools/DevAnalyzers/GlobalSuppressions.cs
new file mode 100644
index 0000000000..9428b904b8
--- /dev/null
+++ b/src/tools/DevAnalyzers/GlobalSuppressions.cs
@@ -0,0 +1,8 @@
+// This file is used by Code Analysis to maintain SuppressMessage
+// attributes that are applied to this project.
+// Project-level suppressions either have no target or are given
+// a specific target and scoped to a namespace, type, member, etc.
+
+using System.Diagnostics.CodeAnalysis;
+
+[assembly: SuppressMessage("MicrosoftCodeAnalysisReleaseTracking", "RS2008:Enable analyzer release tracking")]
diff --git a/tests/Avalonia.Benchmarks/Avalonia.Benchmarks.csproj b/tests/Avalonia.Benchmarks/Avalonia.Benchmarks.csproj
index 3994d20c68..2dbb054221 100644
--- a/tests/Avalonia.Benchmarks/Avalonia.Benchmarks.csproj
+++ b/tests/Avalonia.Benchmarks/Avalonia.Benchmarks.csproj
@@ -19,7 +19,7 @@
-
+
diff --git a/tests/Avalonia.Benchmarks/Base/AvaloniaObject_Binding.cs b/tests/Avalonia.Benchmarks/Base/AvaloniaObject_Binding.cs
new file mode 100644
index 0000000000..885e202544
--- /dev/null
+++ b/tests/Avalonia.Benchmarks/Base/AvaloniaObject_Binding.cs
@@ -0,0 +1,149 @@
+using System.Runtime.CompilerServices;
+using Avalonia.Data;
+using BenchmarkDotNet.Attributes;
+
+#nullable enable
+
+namespace Avalonia.Benchmarks.Base
+{
+ [MemoryDiagnoser]
+ public class AvaloniaObject_Binding
+ {
+ private static TestClass _target = null!;
+ private static TestBindingObservable s_stringSource = new();
+ private static TestBindingObservable s_struct1Source = new();
+ private static TestBindingObservable s_struct2Source = new();
+ private static TestBindingObservable s_struct3Source = new();
+ private static TestBindingObservable s_struct4Source = new();
+ private static TestBindingObservable s_struct5Source = new();
+ private static TestBindingObservable s_struct6Source = new();
+ private static TestBindingObservable s_struct7Source = new();
+ private static TestBindingObservable s_struct8Source = new();
+
+ public AvaloniaObject_Binding()
+ {
+ RuntimeHelpers.RunClassConstructor(typeof(TestClass).TypeHandle);
+ }
+
+ [GlobalSetup]
+ public void Setup()
+ {
+ _target = new TestClass();
+ }
+
+ [Benchmark]
+ public void Setup_Dispose_LocalValue_Bindings()
+ {
+ var target = _target;
+
+ for (var i = 0; i < 100; ++i)
+ {
+ using var s0 = target.Bind(TestClass.StringProperty, s_stringSource);
+ using var s1 = target.Bind(TestClass.Struct1Property, s_struct1Source);
+ using var s2 = target.Bind(TestClass.Struct2Property, s_struct2Source);
+ using var s3 = target.Bind(TestClass.Struct3Property, s_struct3Source);
+ using var s4 = target.Bind(TestClass.Struct4Property, s_struct4Source);
+ using var s5 = target.Bind(TestClass.Struct5Property, s_struct5Source);
+ using var s6 = target.Bind(TestClass.Struct6Property, s_struct6Source);
+ using var s7 = target.Bind(TestClass.Struct7Property, s_struct7Source);
+ using var s8 = target.Bind(TestClass.Struct8Property, s_struct8Source);
+ }
+ }
+
+
+ [Benchmark]
+ public void Fire_LocalValue_Bindings()
+ {
+ var target = _target;
+
+ using var s0 = target.Bind(TestClass.StringProperty, s_stringSource);
+ using var s1 = target.Bind(TestClass.Struct1Property, s_struct1Source);
+ using var s2 = target.Bind(TestClass.Struct2Property, s_struct2Source);
+ using var s3 = target.Bind(TestClass.Struct3Property, s_struct3Source);
+ using var s4 = target.Bind(TestClass.Struct4Property, s_struct4Source);
+ using var s5 = target.Bind(TestClass.Struct5Property, s_struct5Source);
+ using var s6 = target.Bind(TestClass.Struct6Property, s_struct6Source);
+ using var s7 = target.Bind(TestClass.Struct7Property, s_struct7Source);
+ using var s8 = target.Bind(TestClass.Struct8Property, s_struct8Source);
+
+ for (var i = 0; i < 100; ++i)
+ {
+ s_stringSource.OnNext(i.ToString());
+ s_struct1Source.OnNext(new(i + 1));
+ s_struct2Source.OnNext(new(i + 1));
+ s_struct3Source.OnNext(new(i + 1));
+ s_struct4Source.OnNext(new(i + 1));
+ s_struct5Source.OnNext(new(i + 1));
+ s_struct6Source.OnNext(new(i + 1));
+ s_struct7Source.OnNext(new(i + 1));
+ s_struct8Source.OnNext(new(i + 1));
+ }
+ }
+
+ [GlobalSetup(Target = nameof(Fire_LocalValue_Bindings_With_Style_Values))]
+ public void SetupStyleValues()
+ {
+ _target = new TestClass();
+ _target.SetValue(TestClass.StringProperty, "foo", BindingPriority.Style);
+ _target.SetValue(TestClass.Struct1Property, new(), BindingPriority.Style);
+ _target.SetValue(TestClass.Struct2Property, new(), BindingPriority.Style);
+ _target.SetValue(TestClass.Struct3Property, new(), BindingPriority.Style);
+ _target.SetValue(TestClass.Struct4Property, new(), BindingPriority.Style);
+ _target.SetValue(TestClass.Struct5Property, new(), BindingPriority.Style);
+ _target.SetValue(TestClass.Struct6Property, new(), BindingPriority.Style);
+ _target.SetValue(TestClass.Struct7Property, new(), BindingPriority.Style);
+ _target.SetValue(TestClass.Struct8Property, new(), BindingPriority.Style);
+ }
+
+ [Benchmark]
+ public void Fire_LocalValue_Bindings_With_Style_Values()
+ {
+ var target = _target;
+
+ using var s0 = target.Bind(TestClass.StringProperty, s_stringSource);
+ using var s1 = target.Bind(TestClass.Struct1Property, s_struct1Source);
+ using var s2 = target.Bind(TestClass.Struct2Property, s_struct2Source);
+ using var s3 = target.Bind(TestClass.Struct3Property, s_struct3Source);
+ using var s4 = target.Bind(TestClass.Struct4Property, s_struct4Source);
+ using var s5 = target.Bind(TestClass.Struct5Property, s_struct5Source);
+ using var s6 = target.Bind(TestClass.Struct6Property, s_struct6Source);
+ using var s7 = target.Bind(TestClass.Struct7Property, s_struct7Source);
+ using var s8 = target.Bind(TestClass.Struct8Property, s_struct8Source);
+
+ for (var i = 0; i < 100; ++i)
+ {
+ s_stringSource.OnNext(i.ToString());
+ s_struct1Source.OnNext(new(i + 1));
+ s_struct2Source.OnNext(new(i + 1));
+ s_struct3Source.OnNext(new(i + 1));
+ s_struct4Source.OnNext(new(i + 1));
+ s_struct5Source.OnNext(new(i + 1));
+ s_struct6Source.OnNext(new(i + 1));
+ s_struct7Source.OnNext(new(i + 1));
+ s_struct8Source.OnNext(new(i + 1));
+ }
+ }
+
+ private class TestClass : AvaloniaObject
+ {
+ public static readonly StyledProperty StringProperty =
+ AvaloniaProperty.Register("String");
+ public static readonly StyledProperty Struct1Property =
+ AvaloniaProperty.Register("Struct1");
+ public static readonly StyledProperty Struct2Property =
+ AvaloniaProperty.Register("Struct2");
+ public static readonly StyledProperty Struct3Property =
+ AvaloniaProperty.Register("Struct3");
+ public static readonly StyledProperty Struct4Property =
+ AvaloniaProperty.Register("Struct4");
+ public static readonly StyledProperty Struct5Property =
+ AvaloniaProperty.Register("Struct5");
+ public static readonly StyledProperty Struct6Property =
+ AvaloniaProperty.Register("Struct6");
+ public static readonly StyledProperty Struct7Property =
+ AvaloniaProperty.Register("Struct7");
+ public static readonly StyledProperty Struct8Property =
+ AvaloniaProperty.Register("Struct8");
+ }
+ }
+}
diff --git a/tests/Avalonia.Benchmarks/Base/AvaloniaObject_Construct.cs b/tests/Avalonia.Benchmarks/Base/AvaloniaObject_Construct.cs
new file mode 100644
index 0000000000..1f07d4e867
--- /dev/null
+++ b/tests/Avalonia.Benchmarks/Base/AvaloniaObject_Construct.cs
@@ -0,0 +1,81 @@
+using System.Runtime.CompilerServices;
+using BenchmarkDotNet.Attributes;
+
+#nullable enable
+
+namespace Avalonia.Benchmarks.Base
+{
+ [MemoryDiagnoser]
+ public class AvaloniaObject_Construct
+ {
+ public AvaloniaObject_Construct()
+ {
+ RuntimeHelpers.RunClassConstructor(typeof(TestClass).TypeHandle);
+ }
+
+ [Benchmark(Baseline = true)]
+ public void ConstructClrObject_And_Set_Values()
+ {
+ var target = new BaselineTestClass();
+ target.StringProperty = "foo";
+ target.Struct1Property = new(1);
+ target.Struct2Property = new(1);
+ target.Struct3Property = new(1);
+ target.Struct4Property = new(1);
+ target.Struct5Property = new(1);
+ target.Struct6Property = new(1);
+ target.Struct7Property = new(1);
+ target.Struct8Property = new(1);
+ }
+
+ [Benchmark]
+ public void Construct_And_Set_Values()
+ {
+ var target = new TestClass();
+ target.SetValue(TestClass.StringProperty, "foo");
+ target.SetValue(TestClass.Struct1Property, new(1));
+ target.SetValue(TestClass.Struct2Property, new(1));
+ target.SetValue(TestClass.Struct3Property, new(1));
+ target.SetValue(TestClass.Struct4Property, new(1));
+ target.SetValue(TestClass.Struct5Property, new(1));
+ target.SetValue(TestClass.Struct6Property, new(1));
+ target.SetValue(TestClass.Struct7Property, new(1));
+ target.SetValue(TestClass.Struct8Property, new(1));
+ }
+
+ private class TestClass : AvaloniaObject
+ {
+ public static readonly StyledProperty StringProperty =
+ AvaloniaProperty.Register("String");
+ public static readonly StyledProperty Struct1Property =
+ AvaloniaProperty.Register("Struct1");
+ public static readonly StyledProperty Struct2Property =
+ AvaloniaProperty.Register("Struct2");
+ public static readonly StyledProperty Struct3Property =
+ AvaloniaProperty.Register("Struct3");
+ public static readonly StyledProperty Struct4Property =
+ AvaloniaProperty.Register("Struct4");
+ public static readonly StyledProperty Struct5Property =
+ AvaloniaProperty.Register("Struct5");
+ public static readonly StyledProperty Struct6Property =
+ AvaloniaProperty.Register("Struct6");
+ public static readonly StyledProperty Struct7Property =
+ AvaloniaProperty.Register("Struct7");
+ public static readonly StyledProperty Struct8Property =
+ AvaloniaProperty.Register("Struct8");
+ }
+
+ private class BaselineTestClass
+ {
+ public string? StringProperty { get; set; }
+ public Struct1 Struct1Property { get; set; }
+ public Struct2 Struct2Property { get; set; }
+ public Struct3 Struct3Property { get; set; }
+ public Struct4 Struct4Property { get; set; }
+ public Struct5 Struct5Property { get; set; }
+ public Struct6 Struct6Property { get; set; }
+ public Struct7 Struct7Property { get; set; }
+ public Struct8 Struct8Property { get; set; }
+ }
+ }
+}
diff --git a/tests/Avalonia.Benchmarks/Base/AvaloniaObject_GetObservable.cs b/tests/Avalonia.Benchmarks/Base/AvaloniaObject_GetObservable.cs
new file mode 100644
index 0000000000..9157dd156c
--- /dev/null
+++ b/tests/Avalonia.Benchmarks/Base/AvaloniaObject_GetObservable.cs
@@ -0,0 +1,171 @@
+using System;
+using System.Runtime.CompilerServices;
+using BenchmarkDotNet.Attributes;
+
+#nullable enable
+
+namespace Avalonia.Benchmarks.Base
+{
+ [MemoryDiagnoser]
+ public class AvaloniaObject_GetObservable
+ {
+ private TestClass _target = null!;
+ public static int result;
+
+ public AvaloniaObject_GetObservable()
+ {
+ RuntimeHelpers.RunClassConstructor(typeof(TestClass).TypeHandle);
+ }
+
+ [GlobalSetup]
+ public void Setup()
+ {
+ _target = new();
+ }
+
+ [Benchmark(Baseline = true)]
+ public void PropertyChangedSubscription()
+ {
+ var target = _target;
+
+ static void ChangeHandler(object? sender, AvaloniaPropertyChangedEventArgs e)
+ {
+ if (e.Property == TestClass.StringProperty)
+ {
+ var ev = (AvaloniaPropertyChangedEventArgs)e;
+ result += ev.NewValue.Value?.Length ?? 0;
+ }
+ else if (e.Property == TestClass.Struct1Property)
+ {
+ var ev = (AvaloniaPropertyChangedEventArgs)e;
+ result += ev.NewValue.Value.Int1;
+ }
+ else if (e.Property == TestClass.Struct2Property)
+ {
+ var ev = (AvaloniaPropertyChangedEventArgs)e;
+ result += ev.NewValue.Value.Int1;
+ }
+ else if (e.Property == TestClass.Struct3Property)
+ {
+ var ev = (AvaloniaPropertyChangedEventArgs)e;
+ result += ev.NewValue.Value.Int1;
+ }
+ else if (e.Property == TestClass.Struct4Property)
+ {
+ var ev = (AvaloniaPropertyChangedEventArgs)e;
+ result += ev.NewValue.Value.Int1;
+ }
+ else if (e.Property == TestClass.Struct5Property)
+ {
+ var ev = (AvaloniaPropertyChangedEventArgs)e;
+ result += ev.NewValue.Value.Int1;
+ }
+ else if (e.Property == TestClass.Struct6Property)
+ {
+ var ev = (AvaloniaPropertyChangedEventArgs)e;
+ result += ev.NewValue.Value.Int1;
+ }
+ else if (e.Property == TestClass.Struct7Property)
+ {
+ var ev = (AvaloniaPropertyChangedEventArgs)e;
+ result += ev.NewValue.Value.Int1;
+ }
+ else if (e.Property == TestClass.Struct8Property)
+ {
+ var ev = (AvaloniaPropertyChangedEventArgs)e;
+ result += ev.NewValue.Value.Int1;
+ }
+ }
+
+ target.PropertyChanged += ChangeHandler;
+
+ // GetObservable fires with the initial value so to compare like-for-like we also need
+ // to get the initial value here.
+ result += target.GetValue(TestClass.StringProperty)?.Length ?? 0;
+ result += target.GetValue(TestClass.Struct1Property).Int1;
+ result += target.GetValue(TestClass.Struct2Property).Int1;
+ result += target.GetValue(TestClass.Struct3Property).Int1;
+ result += target.GetValue(TestClass.Struct4Property).Int1;
+ result += target.GetValue(TestClass.Struct5Property).Int1;
+ result += target.GetValue(TestClass.Struct6Property).Int1;
+ result += target.GetValue(TestClass.Struct7Property).Int1;
+ result += target.GetValue(TestClass.Struct8Property).Int1;
+
+ for (var i = 0; i < 100; ++i)
+ {
+ target.SetValue(TestClass.StringProperty, "foo" + i);
+ target.SetValue(TestClass.Struct1Property, new(i + 1));
+ target.SetValue(TestClass.Struct2Property, new(i + 1));
+ target.SetValue(TestClass.Struct3Property, new(i + 1));
+ target.SetValue(TestClass.Struct4Property, new(i + 1));
+ target.SetValue(TestClass.Struct5Property, new(i + 1));
+ target.SetValue(TestClass.Struct6Property, new(i + 1));
+ target.SetValue(TestClass.Struct7Property, new(i + 1));
+ target.SetValue(TestClass.Struct8Property, new(i + 1));
+ }
+
+ target.PropertyChanged -= ChangeHandler;
+ }
+
+ [Benchmark]
+ public void GetObservables()
+ {
+ var target = _target;
+
+ var sub1 = target.GetObservable(TestClass.StringProperty).Subscribe(x => result += x?.Length ?? 0);
+ var sub2 = target.GetObservable(TestClass.Struct1Property).Subscribe(x => result += x.Int1);
+ var sub3 = target.GetObservable(TestClass.Struct2Property).Subscribe(x => result += x.Int1);
+ var sub4 = target.GetObservable(TestClass.Struct3Property).Subscribe(x => result += x.Int1);
+ var sub5 = target.GetObservable(TestClass.Struct4Property).Subscribe(x => result += x.Int1);
+ var sub6 = target.GetObservable(TestClass.Struct5Property).Subscribe(x => result += x.Int1);
+ var sub7 = target.GetObservable(TestClass.Struct6Property).Subscribe(x => result += x.Int1);
+ var sub8 = target.GetObservable(TestClass.Struct7Property).Subscribe(x => result += x.Int1);
+ var sub9 = target.GetObservable(TestClass.Struct8Property).Subscribe(x => result += x.Int1);
+
+ for (var i = 0; i < 100; ++i)
+ {
+ target.SetValue(TestClass.StringProperty, "foo" + i);
+ target.SetValue(TestClass.Struct1Property, new(i + 1));
+ target.SetValue(TestClass.Struct2Property, new(i + 1));
+ target.SetValue(TestClass.Struct3Property, new(i + 1));
+ target.SetValue(TestClass.Struct4Property, new(i + 1));
+ target.SetValue(TestClass.Struct5Property, new(i + 1));
+ target.SetValue(TestClass.Struct6Property, new(i + 1));
+ target.SetValue(TestClass.Struct7Property, new(i + 1));
+ target.SetValue(TestClass.Struct8Property, new(i + 1));
+ }
+
+ sub1.Dispose();
+ sub2.Dispose();
+ sub3.Dispose();
+ sub4.Dispose();
+ sub5.Dispose();
+ sub6.Dispose();
+ sub7.Dispose();
+ sub8.Dispose();
+ sub9.Dispose();
+ }
+
+ private class TestClass : AvaloniaObject
+ {
+ public static readonly StyledProperty StringProperty =
+ AvaloniaProperty.Register("String");
+ public static readonly StyledProperty Struct1Property =
+ AvaloniaProperty.Register("Struct1");
+ public static readonly StyledProperty Struct2Property =
+ AvaloniaProperty.Register("Struct2");
+ public static readonly StyledProperty Struct3Property =
+ AvaloniaProperty.Register("Struct3");
+ public static readonly StyledProperty Struct4Property =
+ AvaloniaProperty.Register("Struct4");
+ public static readonly StyledProperty Struct5Property =
+ AvaloniaProperty.Register("Struct5");
+ public static readonly StyledProperty Struct6Property =
+ AvaloniaProperty.Register("Struct6");
+ public static readonly StyledProperty Struct7Property =
+ AvaloniaProperty.Register("Struct7");
+ public static readonly StyledProperty Struct8Property =
+ AvaloniaProperty.Register("Struct8");
+ }
+ }
+}
diff --git a/tests/Avalonia.Benchmarks/Base/AvaloniaObject_GetValue.cs b/tests/Avalonia.Benchmarks/Base/AvaloniaObject_GetValue.cs
new file mode 100644
index 0000000000..0bfea0fafe
--- /dev/null
+++ b/tests/Avalonia.Benchmarks/Base/AvaloniaObject_GetValue.cs
@@ -0,0 +1,181 @@
+using System.Runtime.CompilerServices;
+using Avalonia.Data;
+using BenchmarkDotNet.Attributes;
+
+#nullable enable
+
+namespace Avalonia.Benchmarks.Base
+{
+ [MemoryDiagnoser]
+ public class AvaloniaObject_GetValue
+ {
+ private BaselineTestClass _baseline = new(){ StringProperty = "foo" };
+ private TestClass _target = new();
+
+ public AvaloniaObject_GetValue()
+ {
+ RuntimeHelpers.RunClassConstructor(typeof(TestClass).TypeHandle);
+ }
+
+ [Benchmark(Baseline = true)]
+ public int GetClrPropertyValues()
+ {
+ var target = _baseline;
+ var result = 0;
+
+ for (var i = 0; i < 100; ++i)
+ {
+ result += target.StringProperty?.Length ?? 0;
+ result += target.Struct1Property.Int1;
+ result += target.Struct2Property.Int1;
+ result += target.Struct3Property.Int1;
+ result += target.Struct4Property.Int1;
+ result += target.Struct5Property.Int1;
+ result += target.Struct6Property.Int1;
+ result += target.Struct7Property.Int1;
+ result += target.Struct8Property.Int1;
+ }
+
+ return result;
+ }
+
+ [Benchmark]
+ public int GetDefaultValues()
+ {
+ var target = _target;
+ var result = 0;
+
+ for (var i = 0; i < 100; ++i)
+ {
+ result += target.GetValue(TestClass.StringProperty)?.Length ?? 0;
+ result += target.GetValue(TestClass.Struct1Property).Int1;
+ result += target.GetValue(TestClass.Struct2Property).Int1;
+ result += target.GetValue(TestClass.Struct3Property).Int1;
+ result += target.GetValue(TestClass.Struct4Property).Int1;
+ result += target.GetValue(TestClass.Struct5Property).Int1;
+ result += target.GetValue(TestClass.Struct6Property).Int1;
+ result += target.GetValue(TestClass.Struct7Property).Int1;
+ result += target.GetValue(TestClass.Struct8Property).Int1;
+ }
+
+ return result;
+ }
+
+ [GlobalSetup(Target = nameof(Get_Local_Values))]
+ public void SetupLocalValues()
+ {
+ _target.SetValue(TestClass.StringProperty, "foo");
+ _target.SetValue(TestClass.Struct1Property, new(1));
+ _target.SetValue(TestClass.Struct2Property, new(1));
+ _target.SetValue(TestClass.Struct3Property, new(1));
+ _target.SetValue(TestClass.Struct4Property, new(1));
+ _target.SetValue(TestClass.Struct5Property, new(1));
+ _target.SetValue(TestClass.Struct6Property, new(1));
+ _target.SetValue(TestClass.Struct7Property, new(1));
+ _target.SetValue(TestClass.Struct8Property, new(1));
+ }
+
+ [Benchmark]
+ public int Get_Local_Values()
+ {
+ var target = _target;
+ var result = 0;
+
+ for (var i = 0; i < 100; ++i)
+ {
+ result += target.GetValue(TestClass.StringProperty)?.Length ?? 0;
+ result += target.GetValue(TestClass.Struct1Property).Int1;
+ result += target.GetValue(TestClass.Struct2Property).Int1;
+ result += target.GetValue(TestClass.Struct3Property).Int1;
+ result += target.GetValue(TestClass.Struct4Property).Int1;
+ result += target.GetValue(TestClass.Struct5Property).Int1;
+ result += target.GetValue(TestClass.Struct6Property).Int1;
+ result += target.GetValue(TestClass.Struct7Property).Int1;
+ result += target.GetValue(TestClass.Struct8Property).Int1;
+ }
+
+ return result;
+ }
+
+ [GlobalSetup(Target = nameof(Get_Local_Values_With_Style_Values))]
+ public void SetupLocalValuesAndStyleValues()
+ {
+ var target = _target;
+ target.SetValue(TestClass.StringProperty, "foo");
+ target.SetValue(TestClass.Struct1Property, new(1));
+ target.SetValue(TestClass.Struct2Property, new(1));
+ target.SetValue(TestClass.Struct3Property, new(1));
+ target.SetValue(TestClass.Struct4Property, new(1));
+ target.SetValue(TestClass.Struct5Property, new(1));
+ target.SetValue(TestClass.Struct6Property, new(1));
+ target.SetValue(TestClass.Struct7Property, new(1));
+ target.SetValue(TestClass.Struct8Property, new(1));
+ target.SetValue(TestClass.StringProperty, "bar", BindingPriority.Style);
+ target.SetValue(TestClass.Struct1Property, new(), BindingPriority.Style);
+ target.SetValue(TestClass.Struct2Property, new(), BindingPriority.Style);
+ target.SetValue(TestClass.Struct3Property, new(), BindingPriority.Style);
+ target.SetValue(TestClass.Struct4Property, new(), BindingPriority.Style);
+ target.SetValue(TestClass.Struct5Property, new(), BindingPriority.Style);
+ target.SetValue(TestClass.Struct6Property, new(), BindingPriority.Style);
+ target.SetValue(TestClass.Struct7Property, new(), BindingPriority.Style);
+ target.SetValue(TestClass.Struct8Property, new(), BindingPriority.Style);
+ }
+
+ [Benchmark]
+ public int Get_Local_Values_With_Style_Values()
+ {
+ var target = _target;
+ var result = 0;
+
+ for (var i = 0; i < 100; ++i)
+ {
+ result += target.GetValue(TestClass.StringProperty)?.Length ?? 0;
+ result += target.GetValue(TestClass.Struct1Property).Int1;
+ result += target.GetValue(TestClass.Struct2Property).Int1;
+ result += target.GetValue(TestClass.Struct3Property).Int1;
+ result += target.GetValue(TestClass.Struct4Property).Int1;
+ result += target.GetValue(TestClass.Struct5Property).Int1;
+ result += target.GetValue(TestClass.Struct6Property).Int1;
+ result += target.GetValue(TestClass.Struct7Property).Int1;
+ result += target.GetValue(TestClass.Struct8Property).Int1;
+ }
+
+ return result;
+ }
+
+ private class TestClass : AvaloniaObject
+ {
+ public static readonly StyledProperty StringProperty =
+ AvaloniaProperty.Register("String");
+ public static readonly StyledProperty Struct1Property =
+ AvaloniaProperty.Register("Struct1");
+ public static readonly StyledProperty Struct2Property =
+ AvaloniaProperty.Register("Struct2");
+ public static readonly StyledProperty Struct3Property =
+ AvaloniaProperty.Register("Struct3");
+ public static readonly StyledProperty Struct4Property =
+ AvaloniaProperty.Register("Struct4");
+ public static readonly StyledProperty Struct5Property =
+ AvaloniaProperty.Register("Struct5");
+ public static readonly StyledProperty Struct6Property =
+ AvaloniaProperty.Register("Struct6");
+ public static readonly StyledProperty Struct7Property =
+ AvaloniaProperty.Register("Struct7");
+ public static readonly StyledProperty Struct8Property =
+ AvaloniaProperty.Register("Struct8");
+ }
+
+ private class BaselineTestClass
+ {
+ public string? StringProperty { get; set; }
+ public Struct1 Struct1Property { get; set; }
+ public Struct2 Struct2Property { get; set; }
+ public Struct3 Struct3Property { get; set; }
+ public Struct4 Struct4Property { get; set; }
+ public Struct5 Struct5Property { get; set; }
+ public Struct6 Struct6Property { get; set; }
+ public Struct7 Struct7Property { get; set; }
+ public Struct8 Struct8Property { get; set; }
+ }
+ }
+}
diff --git a/tests/Avalonia.Benchmarks/Base/AvaloniaObject_GetValueInherited.cs b/tests/Avalonia.Benchmarks/Base/AvaloniaObject_GetValueInherited.cs
new file mode 100644
index 0000000000..45e129ecd8
--- /dev/null
+++ b/tests/Avalonia.Benchmarks/Base/AvaloniaObject_GetValueInherited.cs
@@ -0,0 +1,95 @@
+using System;
+using System.Collections.Generic;
+using System.Runtime.CompilerServices;
+using Avalonia.Controls;
+using BenchmarkDotNet.Attributes;
+
+#nullable enable
+
+namespace Avalonia.Benchmarks.Base
+{
+ [MemoryDiagnoser]
+ public class AvaloniaObject_GetValueInherited
+ {
+ private TestClass _root = null!;
+ private TestClass _target = null!;
+
+ public AvaloniaObject_GetValueInherited()
+ {
+ RuntimeHelpers.RunClassConstructor(typeof(TestClass).TypeHandle);
+ }
+
+ [Params(1, 2, 10, 50, 100, 200)]
+ public int Depth { get; set; }
+
+ [GlobalSetup]
+ public void Setup()
+ {
+ _root = new();
+ _root.SetValue(TestClass.StringProperty, "foo");
+ _root.SetValue(TestClass.Struct1Property, new(1));
+ _root.SetValue(TestClass.Struct2Property, new(1));
+ _root.SetValue(TestClass.Struct3Property, new(1));
+ _root.SetValue(TestClass.Struct4Property, new(1));
+ _root.SetValue(TestClass.Struct5Property, new(1));
+ _root.SetValue(TestClass.Struct6Property, new(1));
+ _root.SetValue(TestClass.Struct7Property, new(1));
+ _root.SetValue(TestClass.Struct8Property, new(1));
+
+ var parent = _root;
+
+ for (var i = 0; i < Depth; ++i)
+ {
+ var c = new TestClass();
+ ((ISetLogicalParent)c).SetParent(parent);
+ parent = c;
+ }
+
+ _target = parent;
+ }
+
+ [Benchmark]
+ public int GetInheritedValues()
+ {
+ var target = _target;
+ var result = 0;
+
+ for (var i = 0; i < 100; ++i)
+ {
+ result += target.GetValue(TestClass.StringProperty)?.Length ?? 0;
+ result += target.GetValue(TestClass.Struct1Property).Int1;
+ result += target.GetValue(TestClass.Struct2Property).Int1;
+ result += target.GetValue(TestClass.Struct3Property).Int1;
+ result += target.GetValue(TestClass.Struct4Property).Int1;
+ result += target.GetValue(TestClass.Struct5Property).Int1;
+ result += target.GetValue(TestClass.Struct6Property).Int1;
+ result += target.GetValue(TestClass.Struct7Property).Int1;
+ result += target.GetValue(TestClass.Struct8Property).Int1;
+ }
+
+ return result;
+ }
+
+ private class TestClass : Control
+ {
+ public static readonly StyledProperty StringProperty =
+ AvaloniaProperty.Register("String", inherits: true);
+ public static readonly StyledProperty