Browse Source

Merge branch 'master' into merge-resources

pull/9577/head
Max Katz 3 years ago
committed by GitHub
parent
commit
eaa06db9c8
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 7
      .editorconfig
  2. 5
      .ncrunch/ReactiveUIDemo.v3.ncrunchproject
  3. 1
      Avalonia.Desktop.slnf
  4. 13
      Avalonia.sln
  5. 2
      build/SharedVersion.props
  6. 16
      build/TrimmingEnable.props
  7. 9
      native/Avalonia.Native/src/OSX/WindowImpl.mm
  8. 1
      nukebuild/Build.cs
  9. 2
      nukebuild/_build.csproj
  10. 3
      samples/ControlCatalog/MainView.xaml
  11. 27
      samples/ControlCatalog/Pages/RefreshContainerPage.axaml
  12. 36
      samples/ControlCatalog/Pages/RefreshContainerPage.axaml.cs
  13. 26
      samples/ControlCatalog/ViewModels/RefreshContainerViewModel.cs
  14. 15
      samples/IntegrationTestApp/MainWindow.axaml
  15. 3
      samples/IntegrationTestApp/MainWindow.axaml.cs
  16. 8
      samples/IntegrationTestApp/ShowWindowTest.axaml
  17. 8
      samples/ReactiveUIDemo/App.axaml
  18. 37
      samples/ReactiveUIDemo/App.axaml.cs
  19. 19
      samples/ReactiveUIDemo/MainWindow.axaml
  20. 22
      samples/ReactiveUIDemo/MainWindow.axaml.cs
  21. 28
      samples/ReactiveUIDemo/ReactiveUIDemo.csproj
  22. 11
      samples/ReactiveUIDemo/ViewModels/BarViewModel.cs
  23. 11
      samples/ReactiveUIDemo/ViewModels/FooViewModel.cs
  24. 9
      samples/ReactiveUIDemo/ViewModels/MainWindowViewModel.cs
  25. 21
      samples/ReactiveUIDemo/ViewModels/RoutedViewHostPageViewModel.cs
  26. 16
      samples/ReactiveUIDemo/Views/BarView.axaml
  27. 28
      samples/ReactiveUIDemo/Views/BarView.axaml.cs
  28. 16
      samples/ReactiveUIDemo/Views/FooView.axaml
  29. 28
      samples/ReactiveUIDemo/Views/FooView.axaml.cs
  30. 2
      samples/RenderDemo/Pages/TextFormatterPage.axaml.cs
  31. 2
      src/Android/Avalonia.Android/AndroidInputMethod.cs
  32. 9
      src/Android/Avalonia.Android/AndroidPlatform.cs
  33. 1
      src/Android/Avalonia.Android/Avalonia.Android.csproj
  34. 32
      src/Android/Avalonia.Android/OpenGL/GlPlatformSurface.cs
  35. 30
      src/Android/Avalonia.Android/OpenGL/GlRenderTarget.cs
  36. 18
      src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs
  37. 2
      src/Avalonia.Base/Animation/Animation.cs
  38. 4
      src/Avalonia.Base/Animation/AnimatorKeyFrame.cs
  39. 3
      src/Avalonia.Base/Animation/Animators/Animator`1.cs
  40. 11
      src/Avalonia.Base/Animation/Cue.cs
  41. 10
      src/Avalonia.Base/Avalonia.Base.csproj
  42. 2
      src/Avalonia.Base/AvaloniaProperty.cs
  43. 2
      src/Avalonia.Base/AvaloniaPropertyRegistry.cs
  44. 2
      src/Avalonia.Base/AvaloniaProperty`1.cs
  45. 4
      src/Avalonia.Base/Collections/AvaloniaListConverter.cs
  46. 121
      src/Avalonia.Base/Compatibility/TrimmingAttributes.cs
  47. 5
      src/Avalonia.Base/Data/BindingValue.cs
  48. 2
      src/Avalonia.Base/Data/Converters/DefaultValueConverter.cs
  49. 2
      src/Avalonia.Base/Data/Converters/MethodToCommandConverter.cs
  50. 2
      src/Avalonia.Base/Data/Core/BindingExpression.cs
  51. 5
      src/Avalonia.Base/Data/Core/ExpressionObserver.cs
  52. 4
      src/Avalonia.Base/Data/Core/Parsers/ExpressionTreeParser.cs
  53. 2
      src/Avalonia.Base/Data/Core/Parsers/ExpressionVisitorNodeBuilder.cs
  54. 3
      src/Avalonia.Base/Data/Core/Plugins/AvaloniaPropertyAccessorPlugin.cs
  55. 4
      src/Avalonia.Base/Data/Core/Plugins/DataAnnotationsValidationPlugin.cs
  56. 3
      src/Avalonia.Base/Data/Core/Plugins/ExceptionValidationPlugin.cs
  57. 3
      src/Avalonia.Base/Data/Core/Plugins/IDataValidationPlugin.cs
  58. 3
      src/Avalonia.Base/Data/Core/Plugins/IPropertyAccessorPlugin.cs
  59. 3
      src/Avalonia.Base/Data/Core/Plugins/IStreamPlugin.cs
  60. 3
      src/Avalonia.Base/Data/Core/Plugins/IndeiValidationPlugin.cs
  61. 10
      src/Avalonia.Base/Data/Core/Plugins/InpcPropertyAccessorPlugin.cs
  62. 9
      src/Avalonia.Base/Data/Core/Plugins/MethodAccessorPlugin.cs
  63. 5
      src/Avalonia.Base/Data/Core/Plugins/ObservableStreamPlugin.cs
  64. 7
      src/Avalonia.Base/Data/Core/Plugins/TaskStreamPlugin.cs
  65. 3
      src/Avalonia.Base/Data/Core/PropertyAccessorNode.cs
  66. 2
      src/Avalonia.Base/Data/Core/StreamNode.cs
  67. 30
      src/Avalonia.Base/Diagnostics/TrimmingMessages.cs
  68. 152
      src/Avalonia.Base/Input/GestureRecognizers/PullGestureRecognizer.cs
  69. 8
      src/Avalonia.Base/Input/Gestures.cs
  70. 5
      src/Avalonia.Base/Input/InputElement.cs
  71. 6
      src/Avalonia.Base/Input/Platform/PlatformHotkeyConfiguration.cs
  72. 4
      src/Avalonia.Base/Input/PointerPoint.cs
  73. 43
      src/Avalonia.Base/Input/PullGestureEventArgs.cs
  74. 2
      src/Avalonia.Base/Input/Raw/RawPointerEventArgs.cs
  75. 2
      src/Avalonia.Base/Input/TextInput/ITextInputMethodClient.cs
  76. 2
      src/Avalonia.Base/Logging/ParametrizedLogger.cs
  77. 2
      src/Avalonia.Base/Matrix.cs
  78. 6
      src/Avalonia.Base/Media/BoxShadow.cs
  79. 8
      src/Avalonia.Base/Media/BoxShadows.cs
  80. 2
      src/Avalonia.Base/Media/DrawingContext.cs
  81. 2
      src/Avalonia.Base/Media/FontMetrics.cs
  82. 8
      src/Avalonia.Base/Media/FormattedText.cs
  83. 2
      src/Avalonia.Base/Media/GlyphMetrics.cs
  84. 419
      src/Avalonia.Base/Media/GlyphRun.cs
  85. 20
      src/Avalonia.Base/Media/GlyphRunMetrics.cs
  86. 2
      src/Avalonia.Base/Media/Imaging/RenderTargetBitmap.cs
  87. 2
      src/Avalonia.Base/Media/TextCollapsingCreateInfo.cs
  88. 293
      src/Avalonia.Base/Media/TextFormatting/CharacterBufferRange.cs
  89. 115
      src/Avalonia.Base/Media/TextFormatting/CharacterBufferReference.cs
  90. 13
      src/Avalonia.Base/Media/TextFormatting/FormattedTextSource.cs
  91. 19
      src/Avalonia.Base/Media/TextFormatting/InterWordJustification.cs
  92. 20
      src/Avalonia.Base/Media/TextFormatting/ShapeableTextCharacters.cs
  93. 33
      src/Avalonia.Base/Media/TextFormatting/ShapedBuffer.cs
  94. 19
      src/Avalonia.Base/Media/TextFormatting/ShapedTextCharacters.cs
  95. 4
      src/Avalonia.Base/Media/TextFormatting/SplitResult.cs
  96. 129
      src/Avalonia.Base/Media/TextFormatting/TextCharacters.cs
  97. 102
      src/Avalonia.Base/Media/TextFormatting/TextEllipsisHelper.cs
  98. 4
      src/Avalonia.Base/Media/TextFormatting/TextEndOfLine.cs
  99. 100
      src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs
  100. 2
      src/Avalonia.Base/Media/TextFormatting/TextLayout.cs

7
.editorconfig

@ -64,7 +64,7 @@ dotnet_naming_symbols.static_fields.applicable_kinds = field
dotnet_naming_symbols.static_fields.required_modifiers = static
dotnet_naming_style.static_prefix_style.required_prefix = s_
dotnet_naming_style.static_prefix_style.capitalization = camel_case
dotnet_naming_style.static_prefix_style.capitalization = camel_case
# internal and private fields should be _camelCase
dotnet_naming_rule.camel_case_for_private_internal_fields.severity = suggestion
@ -137,10 +137,15 @@ space_within_single_line_array_initializer_braces = true
#Net Analyzer
dotnet_analyzer_diagnostic.category-Performance.severity = none #error - Uncomment when all violations are fixed.
# CS1591: Missing XML comment for publicly visible type or member
dotnet_diagnostic.CS1591.severity = suggestion
# CA1304: Specify CultureInfo
dotnet_diagnostic.CA1304.severity = warning
# CA1802: Use literals where appropriate
dotnet_diagnostic.CA1802.severity = warning
# CA1815: Override equals and operator equals on value types
dotnet_diagnostic.CA1815.severity = warning
# CA1820: Test for empty strings using string length
dotnet_diagnostic.CA1820.severity = warning
# CA1821: Remove empty finalizers

5
.ncrunch/ReactiveUIDemo.v3.ncrunchproject

@ -0,0 +1,5 @@
<ProjectConfiguration>
<Settings>
<IgnoreThisComponentCompletely>True</IgnoreThisComponentCompletely>
</Settings>
</ProjectConfiguration>

1
Avalonia.Desktop.slnf

@ -9,6 +9,7 @@
"samples\\MiniMvvm\\MiniMvvm.csproj",
"samples\\SampleControls\\ControlSamples.csproj",
"samples\\Sandbox\\Sandbox.csproj",
"samples\\ReactiveUIDemo\\ReactiveUIDemo.csproj",
"src\\Avalonia.Base\\Avalonia.Base.csproj",
"src\\Avalonia.Build.Tasks\\Avalonia.Build.Tasks.csproj",
"src\\Avalonia.Controls.ColorPicker\\Avalonia.Controls.ColorPicker.csproj",

13
Avalonia.sln

@ -40,6 +40,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Shared", "Shared", "{A689DE
.editorconfig = .editorconfig
src\Shared\IsExternalInit.cs = src\Shared\IsExternalInit.cs
src\Shared\ModuleInitializer.cs = src\Shared\ModuleInitializer.cs
src\Shared\NullableAttributes.cs = src\Shared\NullableAttributes.cs
src\Shared\SourceGeneratorAttributes.cs = src\Shared\SourceGeneratorAttributes.cs
src\Avalonia.Base\Compatibility\StringCompatibilityExtensions.cs = src\Avalonia.Base\Compatibility\StringCompatibilityExtensions.cs
EndProjectSection
@ -119,6 +120,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Props", "Props", "{F3AC8BC1
build\SourceLink.props = build\SourceLink.props
build\System.Drawing.Common.props = build\System.Drawing.Common.props
build\System.Memory.props = build\System.Memory.props
build\TrimmingEnable.props = build\TrimmingEnable.props
build\UnitTests.NetFX.props = build\UnitTests.NetFX.props
build\XUnit.props = build\XUnit.props
EndProjectSection
@ -222,14 +224,16 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MobileSandbox.iOS", "sample
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MobileSandbox.Desktop", "samples\MobileSandbox.Desktop\MobileSandbox.Desktop.csproj", "{62D392C9-81CF-487F-92E8-598B2AF3FDCE}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.Browser", "src\Browser\Avalonia.Browser\Avalonia.Browser.csproj", "{4A39637C-9338-4925-A4DB-D072E292EC78}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Browser", "src\Browser\Avalonia.Browser\Avalonia.Browser.csproj", "{4A39637C-9338-4925-A4DB-D072E292EC78}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.Browser.Blazor", "src\Browser\Avalonia.Browser.Blazor\Avalonia.Browser.Blazor.csproj", "{47F8530C-F19B-4B1A-B4D6-EB231522AE5D}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Browser.Blazor", "src\Browser\Avalonia.Browser.Blazor\Avalonia.Browser.Blazor.csproj", "{47F8530C-F19B-4B1A-B4D6-EB231522AE5D}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ControlCatalog.Browser", "samples\ControlCatalog.Browser\ControlCatalog.Browser.csproj", "{15B93A4C-1B46-43F6-B534-7B25B6E99932}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ControlCatalog.Browser.Blazor", "samples\ControlCatalog.Browser.Blazor\ControlCatalog.Browser.Blazor.csproj", "{90B08091-9BBD-4362-B712-E9F2CC62B218}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ReactiveUIDemo", "samples\ReactiveUIDemo\ReactiveUIDemo.csproj", "{75C47156-C5D8-44BC-A5A7-E8657C2248D6}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@ -541,6 +545,10 @@ Global
{90B08091-9BBD-4362-B712-E9F2CC62B218}.Debug|Any CPU.Build.0 = Debug|Any CPU
{90B08091-9BBD-4362-B712-E9F2CC62B218}.Release|Any CPU.ActiveCfg = Release|Any CPU
{90B08091-9BBD-4362-B712-E9F2CC62B218}.Release|Any CPU.Build.0 = Release|Any CPU
{75C47156-C5D8-44BC-A5A7-E8657C2248D6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{75C47156-C5D8-44BC-A5A7-E8657C2248D6}.Debug|Any CPU.Build.0 = Debug|Any CPU
{75C47156-C5D8-44BC-A5A7-E8657C2248D6}.Release|Any CPU.ActiveCfg = Release|Any CPU
{75C47156-C5D8-44BC-A5A7-E8657C2248D6}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@ -605,6 +613,7 @@ Global
{47F8530C-F19B-4B1A-B4D6-EB231522AE5D} = {86A3F706-DC3C-43C6-BE1B-B98F5BAAA268}
{15B93A4C-1B46-43F6-B534-7B25B6E99932} = {9B9E3891-2366-4253-A952-D08BCEB71098}
{90B08091-9BBD-4362-B712-E9F2CC62B218} = {9B9E3891-2366-4253-A952-D08BCEB71098}
{75C47156-C5D8-44BC-A5A7-E8657C2248D6} = {9B9E3891-2366-4253-A952-D08BCEB71098}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {87366D66-1391-4D90-8999-95A620AD786A}

2
build/SharedVersion.props

@ -7,7 +7,7 @@
<PackageProjectUrl>https://avaloniaui.net</PackageProjectUrl>
<RepositoryUrl>https://github.com/AvaloniaUI/Avalonia/</RepositoryUrl>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<NoWarn>CS1591</NoWarn>
<NoWarn>$(NoWarn);CS1591</NoWarn>
<LangVersion>preview</LangVersion>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
<PackageIcon>Icon.png</PackageIcon>

16
build/TrimmingEnable.props

@ -0,0 +1,16 @@
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<SuppressTrimAnalysisWarnings>false</SuppressTrimAnalysisWarnings>
<EnableTrimAnalyzer>true</EnableTrimAnalyzer>
<TrimmerSingleWarn>false</TrimmerSingleWarn>
<IsTrimmable>true</IsTrimmable>
</PropertyGroup>
<!-- Remove check for the AOT when we get rid of dependencies with reflection -->
<PropertyGroup Condition="'$(TargetFramework)' != 'netstandard2.0' and '$(PublishAot)' != 'true'">
<ILLinkTreatWarningsAsErrors>true</ILLinkTreatWarningsAsErrors>
<!-- Trim warnings -->
<WarningsAsErrors>$(WarningsAsErrors);IL2000;IL2001;IL2002;IL2003;IL2004;IL2005;IL2006;IL2007;IL2008;IL2009;IL2010;IL2011;IL2012;IL2013;IL2014;IL2015;IL2016;IL2017;IL2018;IL2019;IL2020;IL2021;IL2022;IL2023;IL2024;IL2025;IL2026;IL2027;IL2028;IL2029;IL2030;IL2031;IL2032;IL2033;IL2034;IL2035;IL2036;IL2037;IL2038;IL2039;IL2040;IL2041;IL2042;IL2043;IL2044;IL2045;IL2046;IL2047;IL2048;IL2049;IL2050;IL2051;IL2052;IL2053;IL2054;IL2055;IL2056;IL2057;IL2058;IL2059;IL2060;IL2061;IL2062;IL2063;IL2064;IL2065;IL2066;IL2067;IL2068;IL2069;IL2070;IL2071;IL2072;IL2073;IL2074;IL2075;IL2076;IL2077;IL2078;IL2079;IL2080;IL2081;IL2082;IL2083;IL2084;IL2085;IL2086;IL2087;IL2088;IL2089;IL2090;IL2091;IL2092;IL2093;IL2094;IL2095;IL2096;IL2097;IL2098;IL2099;IL2100;IL2101;IL2102;IL2103;IL2104;IL2105;IL2106;IL2107;IL2108;IL2109;IL2110;IL2111;IL2112;IL2113;IL2114;IL2115;IL2116;IL2117;IL2118;IL2119;IL2120;IL2121;IL2122;IL2123;IL2124;IL2125;IL2126;IL2127;IL2128;IL2129;IL2130;IL2131;IL2132;IL2133;IL2134;IL2135;IL2136;IL2137;IL2138;IL2139;IL2140;IL2141;IL2142;IL2143;IL2144;IL2145;IL2146;IL2147;IL2148;IL2149;IL2150;IL2151;IL2152;IL2153;IL2154;IL2155;IL2156;IL2157</WarningsAsErrors>
<!-- NativeAOT warnings -->
<WarningsAsErrors>$(WarningsAsErrors);IL3050;IL3051;IL3052;IL3053;IL3054;IL3055;IL3056</WarningsAsErrors>
</PropertyGroup>
</Project>

9
native/Avalonia.Native/src/OSX/WindowImpl.mm

@ -137,7 +137,11 @@ void WindowImpl::BringToFront()
for(auto iterator = _children.begin(); iterator != _children.end(); iterator++)
{
(*iterator)->BringToFront();
auto window = (*iterator)->Window;
// #9565: Only bring window to front if it's on the currently active space
if ([window isOnActiveSpace])
(*iterator)->BringToFront();
}
}
}
@ -161,6 +165,9 @@ void WindowImpl::StartStateTransition() {
void WindowImpl::EndStateTransition() {
_transitioningWindowState = false;
// Ensure correct order of child windows after fullscreen transition.
BringToFront();
}
SystemDecorations WindowImpl::Decorations() {

1
nukebuild/Build.cs

@ -80,7 +80,6 @@ partial class Build : NukeBuild
if (Parameters.IsRunningOnAzure)
c.AddProperty("JavaSdkDirectory", GetVariable<string>("JAVA_HOME_11_X64"));
c.AddProperty("PackageVersion", Parameters.Version)
.AddProperty("iOSRoslynPathHackRequired", true)
.SetConfiguration(Parameters.Configuration)
.SetVerbosity(DotNetVerbosity.Minimal);
return c;

2
nukebuild/_build.csproj

@ -4,7 +4,7 @@
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
<RootNamespace></RootNamespace>
<IsPackable>False</IsPackable>
<NoWarn>CS0649;CS0169;SYSLIB0011</NoWarn>
<NoWarn>$(NoWarn);CS0649;CS0169;SYSLIB0011</NoWarn>
<NukeTelemetryVersion>1</NukeTelemetryVersion>
<TargetFramework>net7.0</TargetFramework>
</PropertyGroup>

3
samples/ControlCatalog/MainView.xaml

@ -135,6 +135,9 @@
<TabItem Header="RadioButton">
<pages:RadioButtonPage />
</TabItem>
<TabItem Header="RefreshContainer">
<pages:RefreshContainerPage />
</TabItem>
<TabItem Header="RelativePanel">
<pages:RelativePanelPage />
</TabItem>

27
samples/ControlCatalog/Pages/RefreshContainerPage.axaml

@ -0,0 +1,27 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:viewModels="using:ControlCatalog.ViewModels"
mc:Ignorable="d"
d:DesignWidth="800"
d:DesignHeight="450"
x:DataType="viewModels:RefreshContainerViewModel"
x:Class="ControlCatalog.Pages.RefreshContainerPage">
<DockPanel HorizontalAlignment="Stretch"
Height="600"
VerticalAlignment="Top">
<Label DockPanel.Dock="Top">A control that supports pull to refresh</Label>
<RefreshContainer Name="Refresh"
DockPanel.Dock="Bottom"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
PullDirection="TopToBottom"
RefreshRequested="RefreshContainerPage_RefreshRequested"
Margin="5">
<ListBox HorizontalAlignment="Stretch"
VerticalAlignment="Top"
Items="{Binding Items}"/>
</RefreshContainer>
</DockPanel>
</UserControl>

36
samples/ControlCatalog/Pages/RefreshContainerPage.axaml.cs

@ -0,0 +1,36 @@
using System.Threading.Tasks;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
using ControlCatalog.ViewModels;
namespace ControlCatalog.Pages
{
public class RefreshContainerPage : UserControl
{
private RefreshContainerViewModel _viewModel;
public RefreshContainerPage()
{
this.InitializeComponent();
_viewModel = new RefreshContainerViewModel();
DataContext = _viewModel;
}
private async void RefreshContainerPage_RefreshRequested(object? sender, RefreshRequestedEventArgs e)
{
var deferral = e.GetDeferral();
await _viewModel.AddToTop();
deferral.Complete();
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
}
}

26
samples/ControlCatalog/ViewModels/RefreshContainerViewModel.cs

@ -0,0 +1,26 @@
using System.Collections.ObjectModel;
using System.Linq;
using System.Reactive;
using System.Threading.Tasks;
using Avalonia.Controls.Notifications;
using ControlCatalog.Pages;
using MiniMvvm;
namespace ControlCatalog.ViewModels
{
public class RefreshContainerViewModel : ViewModelBase
{
public ObservableCollection<string> Items { get; }
public RefreshContainerViewModel()
{
Items = new ObservableCollection<string>(Enumerable.Range(1, 200).Select(i => $"Item {i}"));
}
public async Task AddToTop()
{
await Task.Delay(3000);
Items.Insert(0, $"Item {200 - Items.Count}");
}
}
}

15
samples/IntegrationTestApp/MainWindow.axaml

@ -17,11 +17,15 @@
</NativeMenuItem>
<NativeMenuItem Header="View">
<NativeMenu/>
</NativeMenuItem>
</NativeMenuItem>
</NativeMenu>
</NativeMenu.Menu>
<DockPanel>
<NativeMenuBar DockPanel.Dock="Top"/>
<StackPanel DockPanel.Dock="Bottom" Margin="4" Orientation="Horizontal">
<TextBlock Margin="0,0,4,0">WindowState:</TextBlock>
<TextBlock Name="MainWindowState" Text="{Binding WindowState}"/>
</StackPanel>
<TabControl TabStripPlacement="Left" Name="MainTabs">
<TabItem Header="Automation">
@ -129,13 +133,14 @@
<ComboBoxItem>CenterOwner</ComboBoxItem>
</ComboBox>
<ComboBox Name="ShowWindowState" SelectedIndex="0">
<ComboBoxItem>Normal</ComboBoxItem>
<ComboBoxItem>Minimized</ComboBoxItem>
<ComboBoxItem>Maximized</ComboBoxItem>
<ComboBoxItem>FullScreen</ComboBoxItem>
<ComboBoxItem Name="ShowWindowStateNormal">Normal</ComboBoxItem>
<ComboBoxItem Name="ShowWindowStateMinimized">Minimized</ComboBoxItem>
<ComboBoxItem Name="ShowWindowStateMaximized">Maximized</ComboBoxItem>
<ComboBoxItem Name="ShowWindowStateFullScreen">FullScreen</ComboBoxItem>
</ComboBox>
<Button Name="ShowWindow">Show Window</Button>
<Button Name="SendToBack">Send to Back</Button>
<Button Name="EnterFullscreen">Enter Fullscreen</Button>
<Button Name="ExitFullscreen">Exit Fullscreen</Button>
<Button Name="RestoreAll">Restore All</Button>
</StackPanel>

3
samples/IntegrationTestApp/MainWindow.axaml.cs

@ -1,3 +1,4 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Avalonia;
@ -178,6 +179,8 @@ namespace IntegrationTestApp
ShowWindow();
if (source?.Name == "SendToBack")
SendToBack();
if (source?.Name == "EnterFullscreen")
WindowState = WindowState.FullScreen;
if (source?.Name == "ExitFullscreen")
WindowState = WindowState.Normal;
if (source?.Name == "RestoreAll")

8
samples/IntegrationTestApp/ShowWindowTest.axaml

@ -27,10 +27,10 @@
<Label Grid.Column="0" Grid.Row="7">WindowState</Label>
<ComboBox Name="WindowState" Grid.Column="1" Grid.Row="7" SelectedIndex="{Binding WindowState}">
<ComboBoxItem>Normal</ComboBoxItem>
<ComboBoxItem>Minimized</ComboBoxItem>
<ComboBoxItem>Maximized</ComboBoxItem>
<ComboBoxItem>FullScreen</ComboBoxItem>
<ComboBoxItem Name="WindowStateNormal">Normal</ComboBoxItem>
<ComboBoxItem Name="WindowStateMinimized">Minimized</ComboBoxItem>
<ComboBoxItem Name="WindowStateMaximized">Maximized</ComboBoxItem>
<ComboBoxItem Name="WindowStateFullScreen">FullScreen</ComboBoxItem>
</ComboBox>
<Label Grid.Column="0" Grid.Row="8">Order (mac)</Label>

8
samples/ReactiveUIDemo/App.axaml

@ -0,0 +1,8 @@
<Application
xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="ReactiveUIDemo.App">
<Application.Styles>
<FluentTheme />
</Application.Styles>
</Application>

37
samples/ReactiveUIDemo/App.axaml.cs

@ -0,0 +1,37 @@
using Avalonia;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Markup.Xaml;
using Avalonia.ReactiveUI;
using ReactiveUI;
using ReactiveUIDemo.ViewModels;
using ReactiveUIDemo.Views;
using Splat;
namespace ReactiveUIDemo
{
public class App : Application
{
public override void Initialize()
{
AvaloniaXamlLoader.Load(this);
Locator.CurrentMutable.Register(() => new FooView(), typeof(IViewFor<FooViewModel>));
Locator.CurrentMutable.Register(() => new BarView(), typeof(IViewFor<BarViewModel>));
}
public override void OnFrameworkInitializationCompleted()
{
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
desktop.MainWindow = new MainWindow();
base.OnFrameworkInitializationCompleted();
}
public static int Main(string[] args)
=> BuildAvaloniaApp().StartWithClassicDesktopLifetime(args);
public static AppBuilder BuildAvaloniaApp()
=> AppBuilder.Configure<App>()
.UsePlatformDetect()
.UseReactiveUI()
.LogToTrace();
}
}

19
samples/ReactiveUIDemo/MainWindow.axaml

@ -0,0 +1,19 @@
<Window xmlns="https://github.com/avaloniaui"
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'
x:Class="ReactiveUIDemo.MainWindow"
xmlns:vm="using:ReactiveUIDemo.ViewModels"
xmlns:rxui="using:Avalonia.ReactiveUI"
Title="AvaloniaUI ReactiveUI Demo"
x:DataType="vm:MainWindowViewModel">
<TabControl TabStripPlacement="Left">
<TabItem Header="RoutedViewHost">
<DockPanel DataContext="{Binding RoutedViewHost}">
<StackPanel DockPanel.Dock="Bottom" Orientation="Horizontal" Spacing="8">
<Button Command="{Binding ShowFoo}">Foo</Button>
<Button Command="{Binding ShowBar}">Bar</Button>
</StackPanel>
<rxui:RoutedViewHost Router="{Binding Router}"/>
</DockPanel>
</TabItem>
</TabControl>
</Window>

22
samples/ReactiveUIDemo/MainWindow.axaml.cs

@ -0,0 +1,22 @@
using ReactiveUIDemo.ViewModels;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
namespace ReactiveUIDemo
{
public class MainWindow : Window
{
public MainWindow()
{
this.InitializeComponent();
this.DataContext = new MainWindowViewModel();
this.AttachDevTools();
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
}
}

28
samples/ReactiveUIDemo/ReactiveUIDemo.csproj

@ -0,0 +1,28 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net6.0</TargetFramework>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\Avalonia.Diagnostics\Avalonia.Diagnostics.csproj" />
<ProjectReference Include="..\..\src\Avalonia.ReactiveUI\Avalonia.ReactiveUI.csproj" />
<ProjectReference Include="..\..\src\Avalonia.Themes.Fluent\Avalonia.Themes.Fluent.csproj" />
</ItemGroup>
<ItemGroup>
<Compile Update="Views\BarView.axaml.cs">
<DependentUpon>BarView.axaml</DependentUpon>
</Compile>
<Compile Update="Views\FooView.axaml.cs">
<DependentUpon>FooView.axaml</DependentUpon>
</Compile>
</ItemGroup>
<Import Project="..\..\build\SampleApp.props" />
<Import Project="..\..\build\ReferenceCoreLibraries.props" />
<Import Project="..\..\build\BuildTargets.targets" />
<Import Project="..\..\build\Rx.props" />
<Import Project="..\..\build\ReactiveUI.props" />
</Project>

11
samples/ReactiveUIDemo/ViewModels/BarViewModel.cs

@ -0,0 +1,11 @@
using ReactiveUI;
namespace ReactiveUIDemo.ViewModels
{
internal class BarViewModel : ReactiveObject, IRoutableViewModel
{
public BarViewModel(IScreen screen) => HostScreen = screen;
public string UrlPathSegment => "Bar";
public IScreen HostScreen { get; }
}
}

11
samples/ReactiveUIDemo/ViewModels/FooViewModel.cs

@ -0,0 +1,11 @@
using ReactiveUI;
namespace ReactiveUIDemo.ViewModels
{
internal class FooViewModel : ReactiveObject, IRoutableViewModel
{
public FooViewModel(IScreen screen) => HostScreen = screen;
public string UrlPathSegment => "Foo";
public IScreen HostScreen { get; }
}
}

9
samples/ReactiveUIDemo/ViewModels/MainWindowViewModel.cs

@ -0,0 +1,9 @@
using ReactiveUI;
namespace ReactiveUIDemo.ViewModels
{
internal class MainWindowViewModel : ReactiveObject
{
public RoutedViewHostPageViewModel RoutedViewHost { get; } = new();
}
}

21
samples/ReactiveUIDemo/ViewModels/RoutedViewHostPageViewModel.cs

@ -0,0 +1,21 @@
using ReactiveUI;
namespace ReactiveUIDemo.ViewModels
{
internal class RoutedViewHostPageViewModel : ReactiveObject, IScreen
{
public RoutedViewHostPageViewModel()
{
Foo = new(this);
Bar = new(this);
Router.Navigate.Execute(Foo);
}
public RoutingState Router { get; } = new();
public FooViewModel Foo { get; }
public BarViewModel Bar { get; }
public void ShowFoo() => Router.Navigate.Execute(Foo);
public void ShowBar() => Router.Navigate.Execute(Bar);
}
}

16
samples/ReactiveUIDemo/Views/BarView.axaml

@ -0,0 +1,16 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="ReactiveUIDemo.Views.BarView"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch">
<Border Background="Blue">
<TextBlock HorizontalAlignment="Center"
VerticalAlignment="Center"
FontSize="48">
Bar!
</TextBlock>
</Border>
</UserControl>

28
samples/ReactiveUIDemo/Views/BarView.axaml.cs

@ -0,0 +1,28 @@
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
using ReactiveUI;
using ReactiveUIDemo.ViewModels;
namespace ReactiveUIDemo.Views
{
internal partial class BarView : UserControl, IViewFor<BarViewModel>
{
public BarView()
{
InitializeComponent();
}
public BarViewModel? ViewModel { get; set; }
object? IViewFor.ViewModel
{
get => ViewModel;
set => ViewModel = (BarViewModel?)value;
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
}
}

16
samples/ReactiveUIDemo/Views/FooView.axaml

@ -0,0 +1,16 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="ReactiveUIDemo.Views.FooView"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch">
<Border Background="Red">
<TextBlock HorizontalAlignment="Center"
VerticalAlignment="Center"
FontSize="48">
Foo!
</TextBlock>
</Border>
</UserControl>

28
samples/ReactiveUIDemo/Views/FooView.axaml.cs

@ -0,0 +1,28 @@
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
using ReactiveUI;
using ReactiveUIDemo.ViewModels;
namespace ReactiveUIDemo.Views
{
internal partial class FooView : UserControl, IViewFor<FooViewModel>
{
public FooView()
{
InitializeComponent();
}
public FooViewModel? ViewModel { get; set; }
object? IViewFor.ViewModel
{
get => ViewModel;
set => ViewModel = (FooViewModel?)value;
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
}
}

2
samples/RenderDemo/Pages/TextFormatterPage.axaml.cs

@ -90,7 +90,7 @@ namespace RenderDemo.Pages
return new ControlRun(_control, _defaultProperties);
}
return new TextCharacters(_text.AsMemory(), _defaultProperties);
return new TextCharacters(_text, _defaultProperties);
}
}

2
src/Android/Avalonia.Android/AndroidInputMethod.cs

@ -167,7 +167,7 @@ namespace Avalonia.Android
}
}
public readonly struct ComposingRegion
public readonly record struct ComposingRegion
{
private readonly int _start = -1;
private readonly int _end = -1;

9
src/Android/Avalonia.Android/AndroidPlatform.cs

@ -32,6 +32,7 @@ namespace Avalonia.Android
public static AndroidPlatformOptions Options { get; private set; }
internal static Compositor Compositor { get; private set; }
internal static PlatformRenderInterfaceContextManager RenderInterface { get; private set; }
public static void Initialize()
{
@ -51,15 +52,19 @@ namespace Avalonia.Android
if (Options.UseGpu)
{
EglPlatformOpenGlInterface.TryInitialize();
EglPlatformGraphics.TryInitialize();
}
if (Options.UseCompositor)
{
Compositor = new Compositor(
AvaloniaLocator.Current.GetRequiredService<IRenderLoop>(),
AvaloniaLocator.Current.GetService<IPlatformOpenGlInterface>());
AvaloniaLocator.Current.GetService<IPlatformGraphics>());
}
else
RenderInterface =
new PlatformRenderInterfaceContextManager(AvaloniaLocator.Current
.GetService<IPlatformGraphics>());
}
}

1
src/Android/Avalonia.Android/Avalonia.Android.csproj

@ -16,4 +16,5 @@
<ProjectReference Include="..\..\Skia\Avalonia.Skia\Avalonia.Skia.csproj" />
</ItemGroup>
<Import Project="..\..\..\build\DevAnalyzers.props" />
<Import Project="..\..\..\build\TrimmingEnable.props" />
</Project>

32
src/Android/Avalonia.Android/OpenGL/GlPlatformSurface.cs

@ -1,32 +0,0 @@
using Avalonia.OpenGL;
using Avalonia.OpenGL.Egl;
using Avalonia.OpenGL.Surfaces;
namespace Avalonia.Android.OpenGL
{
internal sealed class GlPlatformSurface : EglGlPlatformSurfaceBase
{
private readonly EglPlatformOpenGlInterface _egl;
private readonly IEglWindowGlPlatformSurfaceInfo _info;
private GlPlatformSurface(EglPlatformOpenGlInterface egl, IEglWindowGlPlatformSurfaceInfo info)
{
_egl = egl;
_info = info;
}
public override IGlPlatformSurfaceRenderTarget CreateGlRenderTarget() =>
new GlRenderTarget(_egl, _info, _egl.CreateWindowSurface(_info.Handle), _info.Handle);
public static GlPlatformSurface TryCreate(IEglWindowGlPlatformSurfaceInfo info)
{
var feature = AvaloniaLocator.Current.GetService<IPlatformOpenGlInterface>();
if (feature is EglPlatformOpenGlInterface egl)
{
return new GlPlatformSurface(egl, info);
}
return null;
}
}
}

30
src/Android/Avalonia.Android/OpenGL/GlRenderTarget.cs

@ -1,30 +0,0 @@
using System;
using Avalonia.OpenGL.Egl;
using Avalonia.OpenGL.Surfaces;
namespace Avalonia.Android.OpenGL
{
internal sealed class GlRenderTarget : EglPlatformSurfaceRenderTargetBase, IGlPlatformSurfaceRenderTargetWithCorruptionInfo
{
private readonly EglGlPlatformSurfaceBase.IEglWindowGlPlatformSurfaceInfo _info;
private readonly EglSurface _surface;
private readonly IntPtr _handle;
public GlRenderTarget(
EglPlatformOpenGlInterface egl,
EglGlPlatformSurfaceBase.IEglWindowGlPlatformSurfaceInfo info,
EglSurface surface,
IntPtr handle)
: base(egl)
{
_info = info;
_surface = surface;
_handle = handle;
}
public bool IsCorrupted => _handle != _info.Handle;
public override IGlPlatformSurfaceRenderingSession BeginDraw() => BeginDraw(_surface, _info);
}
}

18
src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs

@ -8,7 +8,6 @@ using Android.Runtime;
using Android.Text;
using Android.Views;
using Android.Views.InputMethods;
using Avalonia.Android.OpenGL;
using Avalonia.Android.Platform.Specific;
using Avalonia.Android.Platform.Specific.Helpers;
using Avalonia.Android.Platform.Storage;
@ -30,7 +29,7 @@ using AndroidRect = Android.Graphics.Rect;
namespace Avalonia.Android.Platform.SkiaPlatform
{
class TopLevelImpl : IAndroidView, ITopLevelImpl, EglGlPlatformSurfaceBase.IEglWindowGlPlatformSurfaceInfo,
class TopLevelImpl : IAndroidView, ITopLevelImpl, EglGlPlatformSurface.IEglWindowGlPlatformSurfaceInfo,
ITopLevelImplWithTextInputMethod, ITopLevelImplWithNativeControlHost, ITopLevelImplWithStorageProvider
{
private readonly IGlPlatformSurface _gl;
@ -47,7 +46,7 @@ namespace Avalonia.Android.Platform.SkiaPlatform
_textInputMethod = new AndroidInputMethod<ViewImpl>(_view);
_keyboardHelper = new AndroidKeyboardEventsHelper<TopLevelImpl>(this);
_pointerHelper = new AndroidMotionEventsHelper(this);
_gl = GlPlatformSurface.TryCreate(this);
_gl = new EglGlPlatformSurface(this);
_framebuffer = new FramebufferManager(this);
RenderScaling = _view.Scaling;
@ -106,10 +105,15 @@ namespace Avalonia.Android.Platform.SkiaPlatform
public IRenderer CreateRenderer(IRenderRoot root) =>
AndroidPlatform.Options.UseCompositor
? new CompositingRenderer(root, AndroidPlatform.Compositor)
? new CompositingRenderer(root, AndroidPlatform.Compositor, () => Surfaces)
: AndroidPlatform.Options.UseDeferredRendering
? new DeferredRenderer(root, AvaloniaLocator.Current.GetRequiredService<IRenderLoop>()) { RenderOnlyOnRenderThread = true }
: new ImmediateRenderer((Visual)root);
? new DeferredRenderer(root, AvaloniaLocator.Current.GetRequiredService<IRenderLoop>(),
() => AndroidPlatform.RenderInterface.CreateRenderTarget(Surfaces),
AndroidPlatform.RenderInterface)
{ RenderOnlyOnRenderThread = true }
: new ImmediateRenderer((Visual)root,
() => AndroidPlatform.RenderInterface.CreateRenderTarget(Surfaces),
AndroidPlatform.RenderInterface);
public virtual void Hide()
{
@ -283,7 +287,7 @@ namespace Avalonia.Android.Platform.SkiaPlatform
public AcrylicPlatformCompensationLevels AcrylicCompensationLevels => new AcrylicPlatformCompensationLevels(1, 1, 1);
IntPtr EglGlPlatformSurfaceBase.IEglWindowGlPlatformSurfaceInfo.Handle => ((IPlatformHandle)_view).Handle;
IntPtr EglGlPlatformSurface.IEglWindowGlPlatformSurfaceInfo.Handle => ((IPlatformHandle)_view).Handle;
public PixelSize Size => _view.Size;

2
src/Avalonia.Base/Animation/Animation.cs

@ -202,9 +202,7 @@ namespace Avalonia.Animation
/// <param name="setter">The animation setter.</param>
/// <param name="value">The property animator value.</param>
public static void SetAnimator(IAnimationSetter setter,
#if NET6_0_OR_GREATER
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | DynamicallyAccessedMemberTypes.PublicMethods)]
#endif
Type value)
{
s_animators[setter] = (value, () => (IAnimator)Activator.CreateInstance(value)!);

4
src/Avalonia.Base/Animation/AnimatorKeyFrame.cs

@ -1,5 +1,6 @@
using System;
using System.ComponentModel;
using System.Diagnostics.CodeAnalysis;
using Avalonia.Animation.Animators;
using Avalonia.Data;
using Avalonia.Reactive;
@ -66,7 +67,8 @@ namespace Avalonia.Animation
}
}
public T GetTypedValue<T>()
[RequiresUnreferencedCode(TrimmingMessages.TypeConvertionRequiresUnreferencedCodeMessage)]
public T GetTypedValue<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] T>()
{
var typeConv = TypeDescriptor.GetConverter(typeof(T));

3
src/Avalonia.Base/Animation/Animators/Animator`1.cs

@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reactive.Disposables;
using System.Reactive.Linq;
using Avalonia.Animation.Utils;
using Avalonia.Collections;
@ -39,7 +40,7 @@ namespace Avalonia.Animation.Animators
VerifyConvertKeyFrames();
var subject = new DisposeAnimationInstanceSubject<T>(this, animation, control, clock, onComplete);
return match.Subscribe(subject);
return new CompositeDisposable(match.Subscribe(subject), subject);
}
protected T InterpolationHandler(double animationTime, T neutralValue)

11
src/Avalonia.Base/Animation/Cue.cs

@ -8,7 +8,7 @@ namespace Avalonia.Animation
/// Determines the time index for a <see cref="KeyFrame"/>.
/// </summary>
[TypeConverter(typeof(CueTypeConverter))]
public readonly struct Cue : IEquatable<Cue>, IEquatable<double>
public readonly record struct Cue : IEquatable<Cue>, IEquatable<double>
{
/// <summary>
/// The normalized percent value, ranging from 0.0 to 1.0
@ -49,15 +49,6 @@ namespace Avalonia.Animation
}
}
/// <summary>
/// Checks for equality between two <see cref="Cue"/>s.
/// </summary>
/// <param name="other">The second cue.</param>
public bool Equals(Cue other)
{
return CueValue == other.CueValue;
}
/// <summary>
/// Checks for equality between a <see cref="Cue"/>
/// and a <see cref="double"/> value.

10
src/Avalonia.Base/Avalonia.Base.csproj

@ -19,6 +19,7 @@
<Import Project="..\..\build\System.Memory.props" />
<Import Project="..\..\build\ApiDiff.props" />
<Import Project="..\..\build\NullableEnable.props" />
<Import Project="..\..\build\TrimmingEnable.props" />
<Import Project="..\..\build\DevAnalyzers.props" />
<Import Project="..\..\build\SourceGenerators.props" />
<ItemGroup>
@ -30,6 +31,8 @@
<InternalsVisibleTo Include="Avalonia.Base.UnitTests, PublicKey=$(AvaloniaPublicKey)" />
<InternalsVisibleTo Include="Avalonia.Benchmarks, PublicKey=$(AvaloniaPublicKey)" />
<InternalsVisibleTo Include="Avalonia.Controls, PublicKey=$(AvaloniaPublicKey)" />
<InternalsVisibleTo Include="Avalonia.Markup, PublicKey=$(AvaloniaPublicKey)" />
<InternalsVisibleTo Include="Avalonia.Markup.Xaml, PublicKey=$(AvaloniaPublicKey)" />
<InternalsVisibleTo Include="Avalonia.Controls.ColorPicker, PublicKey=$(AvaloniaPublicKey)" />
<InternalsVisibleTo Include="Avalonia.Controls.DataGrid, PublicKey=$(AvaloniaPublicKey)" />
<InternalsVisibleTo Include="Avalonia.Controls.UnitTests, PublicKey=$(AvaloniaPublicKey)" />
@ -41,6 +44,7 @@
<InternalsVisibleTo Include="Avalonia.Skia.UnitTests, PublicKey=$(AvaloniaPublicKey)" />
<InternalsVisibleTo Include="Avalonia.UnitTests, PublicKey=$(AvaloniaPublicKey)" />
<InternalsVisibleTo Include="Avalonia.Benchmarks, PublicKey=$(AvaloniaPublicKey)" />
<InternalsVisibleTo Include="Avalonia.X11, PublicKey=$(AvaloniaPublicKey)" />
<InternalsVisibleTo Include="Avalonia.Win32, PublicKey=$(AvaloniaPublicKey)" />
<InternalsVisibleTo Include="Avalonia.Dialogs, PublicKey=$(AvaloniaPublicKey)" />
<InternalsVisibleTo Include="Avalonia.Diagnostics, PublicKey=$(AvaloniaPublicKey)" />
@ -48,14 +52,10 @@
</ItemGroup>
<ItemGroup Label="Build dependency">
<ProjectReference Include="$(MSBuildThisFileDirectory)\..\Avalonia.Build.Tasks\Avalonia.Build.Tasks.csproj"
SetTargetFramework="TargetFramework=netstandard2.0"
ReferenceOutputAssembly="false"
SkipGetTargetFrameworkProperties="true" />
<ProjectReference Include="$(MSBuildThisFileDirectory)\..\Avalonia.Build.Tasks\Avalonia.Build.Tasks.csproj" SetTargetFramework="TargetFramework=netstandard2.0" ReferenceOutputAssembly="false" SkipGetTargetFrameworkProperties="true" />
</ItemGroup>
<ItemGroup>
<Folder Include="Compatibility\" />
<Folder Include="Rendering\Composition\Utils" />
</ItemGroup>
</Project>

2
src/Avalonia.Base/AvaloniaProperty.cs

@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using Avalonia.Data;
using Avalonia.Data.Core;
using Avalonia.PropertyStore;
@ -442,6 +443,7 @@ namespace Avalonia
/// </summary>
/// <param name="value">The value.</param>
/// <returns>True if the value is valid, otherwise false.</returns>
[RequiresUnreferencedCode(TrimmingMessages.ImplicitTypeConvertionRequiresUnreferencedCodeMessage)]
public bool IsValidValue(object? value)
{
return TypeUtilities.TryConvertImplicit(PropertyType, value, out _);

2
src/Avalonia.Base/AvaloniaPropertyRegistry.cs

@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Runtime.CompilerServices;
namespace Avalonia
@ -42,6 +43,7 @@ namespace Avalonia
/// </summary>
/// <param name="type">The type.</param>
/// <returns>A collection of <see cref="AvaloniaProperty"/> definitions.</returns>
[UnconditionalSuppressMessage("Trimming", "IL2059", Justification = "If type was trimmed out, no properties were referenced")]
public IReadOnlyList<AvaloniaProperty> GetRegistered(Type type)
{
_ = type ?? throw new ArgumentNullException(nameof(type));

2
src/Avalonia.Base/AvaloniaProperty`1.cs

@ -1,4 +1,5 @@
using System;
using System.Diagnostics.CodeAnalysis;
using System.Reactive.Subjects;
using Avalonia.Data;
using Avalonia.Utilities;
@ -67,6 +68,7 @@ namespace Avalonia
protected override IObservable<AvaloniaPropertyChangedEventArgs> GetChanged() => Changed;
[UnconditionalSuppressMessage("Trimming", "IL2026", Justification = TrimmingMessages.ImplicitTypeConvertionSupressWarningMessage)]
protected BindingValue<object?> TryConvert(object? value)
{
if (value == UnsetValue)

4
src/Avalonia.Base/Collections/AvaloniaListConverter.cs

@ -1,5 +1,6 @@
using System;
using System.ComponentModel;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using Avalonia.Utilities;
@ -8,7 +9,8 @@ namespace Avalonia.Collections
/// <summary>
/// Creates an <see cref="AvaloniaList{T}"/> from a string representation.
/// </summary>
public class AvaloniaListConverter<T> : TypeConverter
[RequiresUnreferencedCode(TrimmingMessages.TypeConvertionRequiresUnreferencedCodeMessage)]
public class AvaloniaListConverter<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] T> : TypeConverter
{
public override bool CanConvertFrom(ITypeDescriptorContext? context, Type sourceType)
{

121
src/Avalonia.Base/Compatibility/TrimmingAttributes.cs

@ -0,0 +1,121 @@
#pragma warning disable MA0048 // File name must match type name
// https://github.com/dotnet/runtime/tree/main/src/libraries/System.Private.CoreLib/src/System/Diagnostics/CodeAnalysis
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
namespace System.Diagnostics.CodeAnalysis
{
#nullable enable
#if !NET6_0_OR_GREATER
[AttributeUsage(
AttributeTargets.Field | AttributeTargets.ReturnValue | AttributeTargets.GenericParameter |
AttributeTargets.Parameter | AttributeTargets.Property | AttributeTargets.Method |
AttributeTargets.Class | AttributeTargets.Interface | AttributeTargets.Struct,
Inherited = false)]
internal sealed class DynamicallyAccessedMembersAttribute : Attribute
{
public DynamicallyAccessedMembersAttribute(DynamicallyAccessedMemberTypes memberTypes)
{
MemberTypes = memberTypes;
}
public DynamicallyAccessedMemberTypes MemberTypes { get; }
}
[Flags]
internal enum DynamicallyAccessedMemberTypes
{
None = 0,
PublicParameterlessConstructor = 0x0001,
PublicConstructors = 0x0002 | PublicParameterlessConstructor,
NonPublicConstructors = 0x0004,
PublicMethods = 0x0008,
NonPublicMethods = 0x0010,
PublicFields = 0x0020,
NonPublicFields = 0x0040,
PublicNestedTypes = 0x0080,
NonPublicNestedTypes = 0x0100,
PublicProperties = 0x0200,
NonPublicProperties = 0x0400,
PublicEvents = 0x0800,
NonPublicEvents = 0x1000,
Interfaces = 0x2000,
All = ~None
}
[AttributeUsage(
AttributeTargets.Constructor | AttributeTargets.Field | AttributeTargets.Method,
AllowMultiple = true, Inherited = false)]
internal sealed class DynamicDependencyAttribute : Attribute
{
public DynamicDependencyAttribute(string memberSignature)
{
MemberSignature = memberSignature;
}
public DynamicDependencyAttribute(string memberSignature, Type type)
{
MemberSignature = memberSignature;
Type = type;
}
public DynamicDependencyAttribute(string memberSignature, string typeName, string assemblyName)
{
MemberSignature = memberSignature;
TypeName = typeName;
AssemblyName = assemblyName;
}
public DynamicDependencyAttribute(DynamicallyAccessedMemberTypes memberTypes, Type type)
{
MemberTypes = memberTypes;
Type = type;
}
public DynamicDependencyAttribute(DynamicallyAccessedMemberTypes memberTypes, string typeName, string assemblyName)
{
MemberTypes = memberTypes;
TypeName = typeName;
AssemblyName = assemblyName;
}
public string? MemberSignature { get; }
public DynamicallyAccessedMemberTypes MemberTypes { get; }
public Type? Type { get; }
public string? TypeName { get; }
public string? AssemblyName { get; }
public string? Condition { get; set; }
}
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Constructor | AttributeTargets.Class, Inherited = false)]
internal sealed class RequiresUnreferencedCodeAttribute : Attribute
{
public RequiresUnreferencedCodeAttribute(string message)
{
Message = message;
}
public string Message { get; }
public string? Url { get; set; }
}
[AttributeUsage(AttributeTargets.All, Inherited = false, AllowMultiple = true)]
internal sealed class UnconditionalSuppressMessageAttribute : Attribute
{
public UnconditionalSuppressMessageAttribute(string category, string checkId)
{
Category = category;
CheckId = checkId;
}
public string Category { get; }
public string CheckId { get; }
public string? Scope { get; set; }
public string? Target { get; set; }
public string? MessageId { get; set; }
public string? Justification { get; set; }
}
#endif
}

5
src/Avalonia.Base/Data/BindingValue.cs

@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using Avalonia.Utilities;
namespace Avalonia.Data
@ -79,7 +80,7 @@ namespace Avalonia.Data
/// - For an unset value, use <see cref="Unset"/> or simply `default`
/// - For other types, call one of the static factory methods
/// </remarks>
public readonly struct BindingValue<T>
public readonly record struct BindingValue<T>
{
private readonly T _value;
@ -236,6 +237,7 @@ namespace Avalonia.Data
/// </summary>
/// <param name="value">The untyped value.</param>
/// <returns>The typed binding value.</returns>
[RequiresUnreferencedCode(TrimmingMessages.ImplicitTypeConvertionRequiresUnreferencedCodeMessage)]
public static BindingValue<T> FromUntyped(object? value)
{
return FromUntyped(value, typeof(T));
@ -249,6 +251,7 @@ namespace Avalonia.Data
/// <param name="value">The untyped value.</param>
/// <param name="targetType">The runtime target type.</param>
/// <returns>The typed binding value.</returns>
[RequiresUnreferencedCode(TrimmingMessages.ImplicitTypeConvertionRequiresUnreferencedCodeMessage)]
public static BindingValue<T> FromUntyped(object? value, Type targetType)
{
if (value == AvaloniaProperty.UnsetValue)

2
src/Avalonia.Base/Data/Converters/DefaultValueConverter.cs

@ -1,4 +1,5 @@
using System;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Windows.Input;
using Avalonia.Utilities;
@ -9,6 +10,7 @@ namespace Avalonia.Data.Converters
/// Provides a default set of value conversions for bindings that do not specify a value
/// converter.
/// </summary>
[RequiresUnreferencedCode(TrimmingMessages.TypeConvertionRequiresUnreferencedCodeMessage)]
public class DefaultValueConverter : IValueConverter
{
/// <summary>

2
src/Avalonia.Base/Data/Converters/MethodToCommandConverter.cs

@ -1,5 +1,6 @@
using System;
using System.ComponentModel;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Linq;
using System.Linq.Expressions;
@ -9,6 +10,7 @@ using Avalonia.Utilities;
namespace Avalonia.Data.Converters
{
[RequiresUnreferencedCode(TrimmingMessages.ReflectionBindingRequiresUnreferencedCodeMessage)]
class MethodToCommandConverter : ICommand
{
readonly static Func<object?, bool> AlwaysEnabled = (_) => true;

2
src/Avalonia.Base/Data/Core/BindingExpression.cs

@ -1,4 +1,5 @@
using System;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Reactive.Linq;
using System.Reactive.Subjects;
@ -13,6 +14,7 @@ namespace Avalonia.Data.Core
/// Binds to an expression on an object using a type value converter to convert the values
/// that are sent and received.
/// </summary>
[RequiresUnreferencedCode(TrimmingMessages.TypeConvertionRequiresUnreferencedCodeMessage)]
public class BindingExpression : LightweightObservableBase<object?>, ISubject<object?>, IDescription
{
private readonly ExpressionObserver _inner;

5
src/Avalonia.Base/Data/Core/ExpressionObserver.cs

@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq.Expressions;
using System.Reactive;
using System.Reactive.Linq;
@ -126,6 +127,7 @@ namespace Avalonia.Data.Core
/// <param name="description">
/// A description of the expression. If null, <paramref name="expression"/>'s string representation will be used.
/// </param>
[UnconditionalSuppressMessage("Trimming", "IL2026", Justification = TrimmingMessages.ExpressionSafeSupressWarningMessage)]
public static ExpressionObserver Create<T, U>(
T? root,
Expression<Func<T, U>> expression,
@ -144,6 +146,7 @@ namespace Avalonia.Data.Core
/// <param name="description">
/// A description of the expression. If null, <paramref name="expression"/>'s string representation will be used.
/// </param>
[UnconditionalSuppressMessage("Trimming", "IL2026", Justification = TrimmingMessages.ExpressionSafeSupressWarningMessage)]
public static ExpressionObserver Create<T, U>(
IObservable<T> rootObservable,
Expression<Func<T, U>> expression,
@ -168,6 +171,7 @@ namespace Avalonia.Data.Core
/// <param name="description">
/// A description of the expression. If null, <paramref name="expression"/>'s string representation will be used.
/// </param>
[UnconditionalSuppressMessage("Trimming", "IL2026", Justification = TrimmingMessages.ExpressionSafeSupressWarningMessage)]
public static ExpressionObserver Create<T, U>(
Func<T> rootGetter,
Expression<Func<T, U>> expression,
@ -283,6 +287,7 @@ namespace Avalonia.Data.Core
}
}
[RequiresUnreferencedCode(TrimmingMessages.ExpressionNodeRequiresUnreferencedCodeMessage)]
private static ExpressionNode Parse(LambdaExpression expression, bool enableDataValidation)
{
return ExpressionTreeParser.Parse(expression, enableDataValidation);

4
src/Avalonia.Base/Data/Core/Parsers/ExpressionTreeParser.cs

@ -1,10 +1,12 @@
using System.Linq;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Linq.Expressions;
namespace Avalonia.Data.Core.Parsers
{
static class ExpressionTreeParser
{
[RequiresUnreferencedCode(TrimmingMessages.ExpressionNodeRequiresUnreferencedCodeMessage)]
public static ExpressionNode Parse(Expression expr, bool enableDataValidation)
{
var visitor = new ExpressionVisitorNodeBuilder(enableDataValidation);

2
src/Avalonia.Base/Data/Core/Parsers/ExpressionVisitorNodeBuilder.cs

@ -1,11 +1,13 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
namespace Avalonia.Data.Core.Parsers
{
[RequiresUnreferencedCode(TrimmingMessages.ExpressionNodeRequiresUnreferencedCodeMessage)]
class ExpressionVisitorNodeBuilder : ExpressionVisitor
{
private const string MultiDimensionalArrayGetterMethodName = "Get";

3
src/Avalonia.Base/Data/Core/Plugins/AvaloniaPropertyAccessorPlugin.cs

@ -1,4 +1,5 @@
using System;
using System.Diagnostics.CodeAnalysis;
using System.Runtime.ExceptionServices;
using Avalonia.Utilities;
@ -10,6 +11,7 @@ namespace Avalonia.Data.Core.Plugins
public class AvaloniaPropertyAccessorPlugin : IPropertyAccessorPlugin
{
/// <inheritdoc/>
[RequiresUnreferencedCode(TrimmingMessages.PropertyAccessorsRequiresUnreferencedCodeMessage)]
public bool Match(object obj, string propertyName)
{
if (obj is AvaloniaObject o)
@ -29,6 +31,7 @@ namespace Avalonia.Data.Core.Plugins
/// An <see cref="IPropertyAccessor"/> interface through which future interactions with the
/// property will be made.
/// </returns>
[RequiresUnreferencedCode(TrimmingMessages.PropertyAccessorsRequiresUnreferencedCodeMessage)]
public IPropertyAccessor? Start(WeakReference<object?> reference, string propertyName)
{
_ = reference ?? throw new ArgumentNullException(nameof(reference));

4
src/Avalonia.Base/Data/Core/Plugins/DataAnnotationsValidationPlugin.cs

@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Reflection;
@ -12,6 +13,7 @@ namespace Avalonia.Data.Core.Plugins
public class DataAnnotationsValidationPlugin : IDataValidationPlugin
{
/// <inheritdoc/>
[RequiresUnreferencedCode(TrimmingMessages.DataValidationPluginRequiresUnreferencedCodeMessage)]
public bool Match(WeakReference<object?> reference, string memberName)
{
reference.TryGetTarget(out var target);
@ -24,11 +26,13 @@ namespace Avalonia.Data.Core.Plugins
}
/// <inheritdoc/>
[RequiresUnreferencedCode(TrimmingMessages.DataValidationPluginRequiresUnreferencedCodeMessage)]
public IPropertyAccessor Start(WeakReference<object?> reference, string name, IPropertyAccessor inner)
{
return new Accessor(reference, name, inner);
}
[RequiresUnreferencedCode(TrimmingMessages.DataValidationPluginRequiresUnreferencedCodeMessage)]
private sealed class Accessor : DataValidationBase
{
private readonly ValidationContext? _context;

3
src/Avalonia.Base/Data/Core/Plugins/ExceptionValidationPlugin.cs

@ -1,4 +1,5 @@
using System;
using System.Diagnostics.CodeAnalysis;
using System.Reflection;
namespace Avalonia.Data.Core.Plugins
@ -9,9 +10,11 @@ namespace Avalonia.Data.Core.Plugins
public class ExceptionValidationPlugin : IDataValidationPlugin
{
/// <inheritdoc/>
[RequiresUnreferencedCode(TrimmingMessages.DataValidationPluginRequiresUnreferencedCodeMessage)]
public bool Match(WeakReference<object?> reference, string memberName) => true;
/// <inheritdoc/>
[RequiresUnreferencedCode(TrimmingMessages.DataValidationPluginRequiresUnreferencedCodeMessage)]
public IPropertyAccessor Start(WeakReference<object?> reference, string name, IPropertyAccessor inner)
{
return new Validator(reference, name, inner);

3
src/Avalonia.Base/Data/Core/Plugins/IDataValidationPlugin.cs

@ -1,4 +1,5 @@
using System;
using System.Diagnostics.CodeAnalysis;
namespace Avalonia.Data.Core.Plugins
{
@ -13,6 +14,7 @@ namespace Avalonia.Data.Core.Plugins
/// <param name="reference">A weak reference to the object.</param>
/// <param name="memberName">The name of the member to validate.</param>
/// <returns>True if the plugin can handle the object; otherwise false.</returns>
[RequiresUnreferencedCode(TrimmingMessages.DataValidationPluginRequiresUnreferencedCodeMessage)]
bool Match(WeakReference<object?> reference, string memberName);
/// <summary>
@ -25,6 +27,7 @@ namespace Avalonia.Data.Core.Plugins
/// An <see cref="IPropertyAccessor"/> interface through which future interactions with the
/// property will be made.
/// </returns>
[RequiresUnreferencedCode(TrimmingMessages.DataValidationPluginRequiresUnreferencedCodeMessage)]
IPropertyAccessor Start(WeakReference<object?> reference,
string propertyName,
IPropertyAccessor inner);

3
src/Avalonia.Base/Data/Core/Plugins/IPropertyAccessorPlugin.cs

@ -1,4 +1,5 @@
using System;
using System.Diagnostics.CodeAnalysis;
namespace Avalonia.Data.Core.Plugins
{
@ -14,6 +15,7 @@ namespace Avalonia.Data.Core.Plugins
/// <param name="obj">The object.</param>
/// <param name="propertyName">The property name.</param>
/// <returns>True if the plugin can handle the property on the object; otherwise false.</returns>
[RequiresUnreferencedCode(TrimmingMessages.PropertyAccessorsRequiresUnreferencedCodeMessage)]
bool Match(object obj, string propertyName);
/// <summary>
@ -25,6 +27,7 @@ namespace Avalonia.Data.Core.Plugins
/// An <see cref="IPropertyAccessor"/> interface through which future interactions with the
/// property will be made.
/// </returns>
[RequiresUnreferencedCode(TrimmingMessages.PropertyAccessorsRequiresUnreferencedCodeMessage)]
IPropertyAccessor? Start(WeakReference<object?> reference,
string propertyName);
}

3
src/Avalonia.Base/Data/Core/Plugins/IStreamPlugin.cs

@ -1,4 +1,5 @@
using System;
using System.Diagnostics.CodeAnalysis;
namespace Avalonia.Data.Core.Plugins
{
@ -12,6 +13,7 @@ namespace Avalonia.Data.Core.Plugins
/// </summary>
/// <param name="reference">A weak reference to the value.</param>
/// <returns>True if the plugin can handle the value; otherwise false.</returns>
[RequiresUnreferencedCode(TrimmingMessages.StreamPluginRequiresUnreferencedCodeMessage)]
bool Match(WeakReference<object?> reference);
/// <summary>
@ -21,6 +23,7 @@ namespace Avalonia.Data.Core.Plugins
/// <returns>
/// An observable that produces the output for the value.
/// </returns>
[RequiresUnreferencedCode(TrimmingMessages.StreamPluginRequiresUnreferencedCodeMessage)]
IObservable<object?> Start(WeakReference<object?> reference);
}
}

3
src/Avalonia.Base/Data/Core/Plugins/IndeiValidationPlugin.cs

@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Avalonia.Utilities;
@ -18,6 +19,7 @@ namespace Avalonia.Data.Core.Plugins
);
/// <inheritdoc/>
[RequiresUnreferencedCode(TrimmingMessages.DataValidationPluginRequiresUnreferencedCodeMessage)]
public bool Match(WeakReference<object?> reference, string memberName)
{
reference.TryGetTarget(out var target);
@ -26,6 +28,7 @@ namespace Avalonia.Data.Core.Plugins
}
/// <inheritdoc/>
[RequiresUnreferencedCode(TrimmingMessages.DataValidationPluginRequiresUnreferencedCodeMessage)]
public IPropertyAccessor Start(WeakReference<object?> reference, string name, IPropertyAccessor accessor)
{
return new Validator(reference, name, accessor);

10
src/Avalonia.Base/Data/Core/Plugins/InpcPropertyAccessorPlugin.cs

@ -1,7 +1,7 @@
using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Diagnostics.CodeAnalysis;
using System.Reflection;
using Avalonia.Utilities;
@ -17,6 +17,7 @@ namespace Avalonia.Data.Core.Plugins
new Dictionary<(Type, string), PropertyInfo?>();
/// <inheritdoc/>
[RequiresUnreferencedCode(TrimmingMessages.PropertyAccessorsRequiresUnreferencedCodeMessage)]
public bool Match(object obj, string propertyName) => GetFirstPropertyWithName(obj, propertyName) != null;
/// <summary>
@ -28,6 +29,7 @@ namespace Avalonia.Data.Core.Plugins
/// An <see cref="IPropertyAccessor"/> interface through which future interactions with the
/// property will be made.
/// </returns>
[RequiresUnreferencedCode(TrimmingMessages.PropertyAccessorsRequiresUnreferencedCodeMessage)]
public IPropertyAccessor? Start(WeakReference<object?> reference, string propertyName)
{
_ = reference ?? throw new ArgumentNullException(nameof(reference));
@ -52,7 +54,8 @@ namespace Avalonia.Data.Core.Plugins
private const BindingFlags PropertyBindingFlags =
BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Static | BindingFlags.Instance;
[RequiresUnreferencedCode(TrimmingMessages.PropertyAccessorsRequiresUnreferencedCodeMessage)]
private PropertyInfo? GetFirstPropertyWithName(object instance, string propertyName)
{
if (instance is IReflectableType reflectableType && instance is not Type)
@ -70,7 +73,8 @@ namespace Avalonia.Data.Core.Plugins
return propertyInfo;
}
private PropertyInfo? TryFindAndCacheProperty(Type type, string propertyName)
private PropertyInfo? TryFindAndCacheProperty(
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties | DynamicallyAccessedMemberTypes.NonPublicProperties)] Type type, string propertyName)
{
PropertyInfo? found = null;

9
src/Avalonia.Base/Data/Core/Plugins/MethodAccessorPlugin.cs

@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq.Expressions;
using System.Reflection;
@ -10,8 +11,10 @@ namespace Avalonia.Data.Core.Plugins
private readonly Dictionary<(Type, string), MethodInfo?> _methodLookup =
new Dictionary<(Type, string), MethodInfo?>();
[RequiresUnreferencedCode(TrimmingMessages.PropertyAccessorsRequiresUnreferencedCodeMessage)]
public bool Match(object obj, string methodName) => GetFirstMethodWithName(obj.GetType(), methodName) != null;
[RequiresUnreferencedCode(TrimmingMessages.PropertyAccessorsRequiresUnreferencedCodeMessage)]
public IPropertyAccessor? Start(WeakReference<object?> reference, string methodName)
{
_ = reference ?? throw new ArgumentNullException(nameof(reference));
@ -34,7 +37,8 @@ namespace Avalonia.Data.Core.Plugins
}
}
private MethodInfo? GetFirstMethodWithName(Type type, string methodName)
private MethodInfo? GetFirstMethodWithName(
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods | DynamicallyAccessedMemberTypes.NonPublicMethods)] Type type, string methodName)
{
var key = (type, methodName);
@ -46,7 +50,8 @@ namespace Avalonia.Data.Core.Plugins
return methodInfo;
}
private MethodInfo? TryFindAndCacheMethod(Type type, string methodName)
private MethodInfo? TryFindAndCacheMethod(
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods | DynamicallyAccessedMemberTypes.NonPublicMethods)] Type type, string methodName)
{
MethodInfo? found = null;

5
src/Avalonia.Base/Data/Core/Plugins/ObservableStreamPlugin.cs

@ -1,4 +1,5 @@
using System;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Reactive.Linq;
using System.Reflection;
@ -8,6 +9,7 @@ namespace Avalonia.Data.Core.Plugins
/// <summary>
/// Handles binding to <see cref="IObservable{T}"/>s for the '^' stream binding operator.
/// </summary>
[UnconditionalSuppressMessage("Trimming", "IL3050", Justification = TrimmingMessages.IgnoreNativeAotSupressWarningMessage)]
public class ObservableStreamPlugin : IStreamPlugin
{
static MethodInfo? observableSelect;
@ -17,6 +19,7 @@ namespace Avalonia.Data.Core.Plugins
/// </summary>
/// <param name="reference">A weak reference to the value.</param>
/// <returns>True if the plugin can handle the value; otherwise false.</returns>
[RequiresUnreferencedCode(TrimmingMessages.StreamPluginRequiresUnreferencedCodeMessage)]
public virtual bool Match(WeakReference<object?> reference)
{
reference.TryGetTarget(out var target);
@ -33,6 +36,7 @@ namespace Avalonia.Data.Core.Plugins
/// <returns>
/// An observable that produces the output for the value.
/// </returns>
[RequiresUnreferencedCode(TrimmingMessages.StreamPluginRequiresUnreferencedCodeMessage)]
public virtual IObservable<object?> Start(WeakReference<object?> reference)
{
if (!reference.TryGetTarget(out var target) || target is null)
@ -65,6 +69,7 @@ namespace Avalonia.Data.Core.Plugins
new object[] { target, box })!;
}
[RequiresUnreferencedCode(TrimmingMessages.StreamPluginRequiresUnreferencedCodeMessage)]
private static MethodInfo GetObservableSelect(Type source)
{
return GetObservableSelect().MakeGenericMethod(source, typeof(object));

7
src/Avalonia.Base/Data/Core/Plugins/TaskStreamPlugin.cs

@ -1,4 +1,5 @@
using System;
using System.Diagnostics.CodeAnalysis;
using System.Reactive.Linq;
using System.Reactive.Subjects;
using System.Reflection;
@ -9,6 +10,7 @@ namespace Avalonia.Data.Core.Plugins
/// <summary>
/// Handles binding to <see cref="Task"/>s for the '^' stream binding operator.
/// </summary>
[UnconditionalSuppressMessage("Trimming", "IL3050", Justification = TrimmingMessages.IgnoreNativeAotSupressWarningMessage)]
public class TaskStreamPlugin : IStreamPlugin
{
/// <summary>
@ -16,12 +18,13 @@ namespace Avalonia.Data.Core.Plugins
/// </summary>
/// <param name="reference">A weak reference to the value.</param>
/// <returns>True if the plugin can handle the value; otherwise false.</returns>
[RequiresUnreferencedCode(TrimmingMessages.StreamPluginRequiresUnreferencedCodeMessage)]
public virtual bool Match(WeakReference<object?> reference)
{
reference.TryGetTarget(out var target);
return target is Task;
}
}
/// <summary>
/// Starts producing output based on the specified value.
@ -30,6 +33,7 @@ namespace Avalonia.Data.Core.Plugins
/// <returns>
/// An observable that produces the output for the value.
/// </returns>
[RequiresUnreferencedCode(TrimmingMessages.StreamPluginRequiresUnreferencedCodeMessage)]
public virtual IObservable<object?> Start(WeakReference<object?> reference)
{
reference.TryGetTarget(out var target);
@ -59,6 +63,7 @@ namespace Avalonia.Data.Core.Plugins
return Observable.Empty<object?>();
}
[RequiresUnreferencedCode(TrimmingMessages.StreamPluginRequiresUnreferencedCodeMessage)]
private static IObservable<object?> HandleCompleted(Task task)
{
var resultProperty = task.GetType().GetRuntimeProperty("Result");

3
src/Avalonia.Base/Data/Core/PropertyAccessorNode.cs

@ -1,9 +1,10 @@
using System;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using Avalonia.Data.Core.Plugins;
namespace Avalonia.Data.Core
{
[RequiresUnreferencedCode(TrimmingMessages.ExpressionNodeRequiresUnreferencedCodeMessage)]
public class PropertyAccessorNode : SettableNode
{
private readonly bool _enableValidation;

2
src/Avalonia.Base/Data/Core/StreamNode.cs

@ -1,9 +1,11 @@
using System;
using System.Diagnostics.CodeAnalysis;
using System.Reactive.Linq;
using Avalonia.Data.Core.Plugins;
namespace Avalonia.Data.Core
{
[RequiresUnreferencedCode(TrimmingMessages.ExpressionNodeRequiresUnreferencedCodeMessage)]
public class StreamNode : ExpressionNode
{
private IStreamPlugin? _customPlugin = null;

30
src/Avalonia.Base/Diagnostics/TrimmingMessages.cs

@ -0,0 +1,30 @@
namespace Avalonia;
internal static class TrimmingMessages
{
public const string ImplicitTypeConvertionSupressWarningMessage = "Implicit convertion methods might be removed by the linker. We don't have a reliable way to prevent it, except converting everything in compile time when possible.";
public const string ImplicitTypeConvertionRequiresUnreferencedCodeMessage = "Implicit convertion methods are required for type conversion.";
public const string TypeConvertionSupressWarningMessage = "Convertion methods might be removed by the linker. We don't have a reliable way to prevent it, except converting everything in compile time when possible.";
public const string TypeConvertionRequiresUnreferencedCodeMessage = "Convertion methods are required for type conversion, including op_Implicit, op_Explicit, Parse and TypeConverter.";
public const string ReflectionBindingRequiresUnreferencedCodeMessage = "BindingExpression and ReflectionBinding heavily use reflection. Consider using CompiledBindings instead.";
public const string ReflectionBindingSupressWarningMessage = "BindingExpression and ReflectionBinding internal heavily use reflection.";
public const string CompiledBindingSafeSupressWarningMessage = "CompiledBinding preserves members used in the expression tree.";
public const string ExpressionNodeRequiresUnreferencedCodeMessage = "ExpressionNode might require unreferenced code.";
public const string ExpressionSafeSupressWarningMessage = "Typed Expressions preserves members used in the expression tree.";
public const string SelectorsParseRequiresUnreferencedCodeMessage = "Selectors runtime parser might require unreferenced code. Consider using stronly typed selectors factory with 'new Style(s => s.OfType<Button>())' syntax.";
public const string PropertyAccessorsRequiresUnreferencedCodeMessage = "PropertyAccessors might require unreferenced code.";
public const string DataValidationPluginRequiresUnreferencedCodeMessage = "DataValidationPlugin might require unreferenced code.";
public const string StreamPluginRequiresUnreferencedCodeMessage = "StreamPlugin might require unreferenced code.";
public const string StyleResourceIncludeRequiresUnreferenceCodeMessage = "StyleInclude and ResourceInclude use AvaloniaXamlLoader.Load which dynamically loads referenced assembly with Avalonia resources. Note, StyleInclude and ResourceInclude defined in XAML are resolved compile time and are safe with trimming and AOT.";
public const string AvaloniaXamlLoaderRequiresUnreferenceCodeMessage = "AvaloniaXamlLoader.Load(uri, baseUri) dynamically loads referenced assembly with Avalonia resources.";
public const string XamlTypeResolvedRequiresUnreferenceCodeMessage = "XamlTypeResolver might require unreferenced code.";
public const string IgnoreNativeAotSupressWarningMessage = "This method is not supported by NativeAOT.";
}

152
src/Avalonia.Base/Input/GestureRecognizers/PullGestureRecognizer.cs

@ -0,0 +1,152 @@
using Avalonia.Input.GestureRecognizers;
namespace Avalonia.Input
{
public class PullGestureRecognizer : StyledElement, IGestureRecognizer
{
private IInputElement? _target;
private IGestureRecognizerActionsDispatcher? _actions;
private Point _initialPosition;
private int _gestureId;
private IPointer? _tracking;
private PullDirection _pullDirection;
/// <summary>
/// Defines the <see cref="PullDirection"/> property.
/// </summary>
public static readonly DirectProperty<PullGestureRecognizer, PullDirection> PullDirectionProperty =
AvaloniaProperty.RegisterDirect<PullGestureRecognizer, PullDirection>(
nameof(PullDirection),
o => o.PullDirection,
(o, v) => o.PullDirection = v);
public PullDirection PullDirection
{
get => _pullDirection;
set => SetAndRaise(PullDirectionProperty, ref _pullDirection, value);
}
public PullGestureRecognizer(PullDirection pullDirection)
{
PullDirection = pullDirection;
}
public void Initialize(IInputElement target, IGestureRecognizerActionsDispatcher actions)
{
_target = target;
_actions = actions;
_target?.AddHandler(InputElement.PointerPressedEvent, OnPointerPressed, Interactivity.RoutingStrategies.Tunnel | Interactivity.RoutingStrategies.Bubble);
_target?.AddHandler(InputElement.PointerReleasedEvent, OnPointerReleased, Interactivity.RoutingStrategies.Tunnel | Interactivity.RoutingStrategies.Bubble);
}
private void OnPointerPressed(object? sender, PointerPressedEventArgs e)
{
PointerPressed(e);
}
private void OnPointerReleased(object? sender, PointerReleasedEventArgs e)
{
PointerReleased(e);
}
public void PointerCaptureLost(IPointer pointer)
{
if (_tracking == pointer)
{
EndPull();
}
}
public void PointerMoved(PointerEventArgs e)
{
if (_tracking == e.Pointer && _target is Visual visual)
{
var currentPosition = e.GetPosition(visual);
_actions!.Capture(e.Pointer, this);
Vector delta = default;
switch (PullDirection)
{
case PullDirection.TopToBottom:
if (currentPosition.Y > _initialPosition.Y)
{
delta = new Vector(0, currentPosition.Y - _initialPosition.Y);
}
break;
case PullDirection.BottomToTop:
if (currentPosition.Y < _initialPosition.Y)
{
delta = new Vector(0, _initialPosition.Y - currentPosition.Y);
}
break;
case PullDirection.LeftToRight:
if (currentPosition.X > _initialPosition.X)
{
delta = new Vector(currentPosition.X - _initialPosition.X, 0);
}
break;
case PullDirection.RightToLeft:
if (currentPosition.X < _initialPosition.X)
{
delta = new Vector(_initialPosition.X - currentPosition.X, 0);
}
break;
}
_target?.RaiseEvent(new PullGestureEventArgs(_gestureId, delta, PullDirection));
}
}
public void PointerPressed(PointerPressedEventArgs e)
{
if (_target != null && _target is Visual visual && (e.Pointer.Type == PointerType.Touch || e.Pointer.Type == PointerType.Pen))
{
var position = e.GetPosition(visual);
var canPull = false;
var bounds = visual.Bounds;
switch (PullDirection)
{
case PullDirection.TopToBottom:
canPull = position.Y < bounds.Height * 0.1;
break;
case PullDirection.BottomToTop:
canPull = position.Y > bounds.Height - (bounds.Height * 0.1);
break;
case PullDirection.LeftToRight:
canPull = position.X < bounds.Width * 0.1;
break;
case PullDirection.RightToLeft:
canPull = position.X > bounds.Width - (bounds.Width * 0.1);
break;
}
if (canPull)
{
_gestureId = PullGestureEventArgs.GetNextFreeId();
_tracking = e.Pointer;
_initialPosition = position;
}
}
}
public void PointerReleased(PointerReleasedEventArgs e)
{
if (_tracking == e.Pointer)
{
EndPull();
}
}
private void EndPull()
{
_tracking = null;
_initialPosition = default;
_target?.RaiseEvent(new PullGestureEndedEventArgs(_gestureId, PullDirection));
}
}
}

8
src/Avalonia.Base/Input/Gestures.cs

@ -46,6 +46,14 @@ namespace Avalonia.Input
private static readonly WeakReference<object?> s_lastPress = new WeakReference<object?>(null);
private static Point s_lastPressPoint;
public static readonly RoutedEvent<PullGestureEventArgs> PullGestureEvent =
RoutedEvent.Register<PullGestureEventArgs>(
"PullGesture", RoutingStrategies.Bubble, typeof(Gestures));
public static readonly RoutedEvent<PullGestureEndedEventArgs> PullGestureEndedEvent =
RoutedEvent.Register<PullGestureEndedEventArgs>(
"PullGestureEnded", RoutingStrategies.Bubble, typeof(Gestures));
static Gestures()
{
InputElement.PointerPressedEvent.RouteFinished.Subscribe(PointerPressed);

5
src/Avalonia.Base/Input/InputElement.cs

@ -442,6 +442,11 @@ namespace Avalonia.Input
{
SetAndRaise(IsEffectivelyEnabledProperty, ref _isEffectivelyEnabled, value);
PseudoClasses.Set(":disabled", !value);
if (!IsEffectivelyEnabled && FocusManager.Instance?.Current == this)
{
FocusManager.Instance?.Focus(null);
}
}
}

6
src/Avalonia.Base/Input/Platform/PlatformHotkeyConfiguration.cs

@ -20,7 +20,8 @@ namespace Avalonia.Input.Platform
WholeWordTextActionModifiers = wholeWordTextActionModifiers;
Copy = new List<KeyGesture>
{
new KeyGesture(Key.C, commandModifiers)
new KeyGesture(Key.C, commandModifiers),
new KeyGesture(Key.Insert, KeyModifiers.Control)
};
Cut = new List<KeyGesture>
{
@ -28,7 +29,8 @@ namespace Avalonia.Input.Platform
};
Paste = new List<KeyGesture>
{
new KeyGesture(Key.V, commandModifiers)
new KeyGesture(Key.V, commandModifiers),
new KeyGesture(Key.Insert, KeyModifiers.Shift)
};
Undo = new List<KeyGesture>
{

4
src/Avalonia.Base/Input/PointerPoint.cs

@ -5,7 +5,7 @@ namespace Avalonia.Input
/// <summary>
/// Provides basic properties for the input pointer associated with a single mouse, pen/stylus, or touch contact.
/// </summary>
public struct PointerPoint
public record struct PointerPoint
{
public PointerPoint(IPointer pointer, Point position, PointerPointProperties properties)
{
@ -33,7 +33,7 @@ namespace Avalonia.Input
/// <summary>
/// Provides extended properties for a PointerPoint object.
/// </summary>
public struct PointerPointProperties
public record struct PointerPointProperties
{
/// <summary>
/// Gets a value that indicates whether the pointer input was triggered by the primary action mode of an input device.

43
src/Avalonia.Base/Input/PullGestureEventArgs.cs

@ -0,0 +1,43 @@
using System;
using Avalonia.Interactivity;
namespace Avalonia.Input
{
public class PullGestureEventArgs : RoutedEventArgs
{
public int Id { get; }
public Vector Delta { get; }
public PullDirection PullDirection { get; }
private static int _nextId = 1;
internal static int GetNextFreeId() => _nextId++;
public PullGestureEventArgs(int id, Vector delta, PullDirection pullDirection) : base(Gestures.PullGestureEvent)
{
Id = id;
Delta = delta;
PullDirection = pullDirection;
}
}
public class PullGestureEndedEventArgs : RoutedEventArgs
{
public int Id { get; }
public PullDirection PullDirection { get; }
public PullGestureEndedEventArgs(int id, PullDirection pullDirection) : base(Gestures.PullGestureEndedEvent)
{
Id = id;
PullDirection = pullDirection;
}
}
public enum PullDirection
{
TopToBottom,
BottomToTop,
LeftToRight,
RightToLeft
}
}

2
src/Avalonia.Base/Input/Raw/RawPointerEventArgs.cs

@ -130,7 +130,7 @@ namespace Avalonia.Input.Raw
internal IInputElement? InputHitTestResult { get; set; }
}
public struct RawPointerPoint
public record struct RawPointerPoint
{
/// <summary>
/// Pointer position, in client DIPs.

2
src/Avalonia.Base/Input/TextInput/ITextInputMethodClient.cs

@ -46,7 +46,7 @@ namespace Avalonia.Input.TextInput
void SelectInSurroundingText(int start, int end);
}
public struct TextInputMethodSurroundingText
public record struct TextInputMethodSurroundingText
{
public string Text { get; set; }
public int CursorOffset { get; set; }

2
src/Avalonia.Base/Logging/ParametrizedLogger.cs

@ -5,7 +5,7 @@ namespace Avalonia.Logging
/// <summary>
/// Logger sink parametrized for given logging level.
/// </summary>
public readonly struct ParametrizedLogger
public readonly record struct ParametrizedLogger
{
private readonly ILogSink _sink;
private readonly LogEventLevel _level;

2
src/Avalonia.Base/Matrix.cs

@ -571,7 +571,7 @@ namespace Avalonia
return true;
}
public struct Decomposed
public record struct Decomposed
{
public Vector Translate;
public Vector Scale;

6
src/Avalonia.Base/Media/BoxShadow.cs

@ -171,5 +171,11 @@ namespace Avalonia.Media
public Rect TransformBounds(in Rect rect)
=> IsInset ? rect : rect.Translate(new Vector(OffsetX, OffsetY)).Inflate(Spread + Blur);
public static bool operator ==(BoxShadow left, BoxShadow right) =>
left.Equals(right);
public static bool operator !=(BoxShadow left, BoxShadow right) =>
!(left == right);
}
}

8
src/Avalonia.Base/Media/BoxShadows.cs

@ -62,7 +62,9 @@ namespace Avalonia.Media
}
[EditorBrowsable(EditorBrowsableState.Never)]
#pragma warning disable CA1815 // Override equals and operator equals on value types
public struct BoxShadowsEnumerator
#pragma warning restore CA1815 // Override equals and operator equals on value types
{
private int _index;
private BoxShadows _shadows;
@ -149,5 +151,11 @@ namespace Avalonia.Media
return hashCode;
}
}
public static bool operator ==(BoxShadows left, BoxShadows right) =>
left.Equals(right);
public static bool operator !=(BoxShadows left, BoxShadows right) =>
!(left == right);
}
}

2
src/Avalonia.Base/Media/DrawingContext.cs

@ -261,7 +261,7 @@ namespace Avalonia.Media
DrawRectangle(brush, null, rect, cornerRadius, cornerRadius);
}
public readonly struct PushedState : IDisposable
public readonly record struct PushedState : IDisposable
{
private readonly int _level;
private readonly DrawingContext _context;

2
src/Avalonia.Base/Media/FontMetrics.cs

@ -3,7 +3,7 @@
/// <summary>
/// The font metrics is holding information about a font's ascent, descent, etc. in design em units.
/// </summary>
public readonly struct FontMetrics
public readonly record struct FontMetrics
{
/// <summary>
/// Gets the font design units per em.

8
src/Avalonia.Base/Media/FormattedText.cs

@ -1,10 +1,8 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.Globalization;
using Avalonia.Controls;
using Avalonia.Media.TextFormatting;
using Avalonia.Utilities;
@ -25,7 +23,7 @@ namespace Avalonia.Media
private const double MaxFontEmSize = RealInfiniteWidth / GreatestMultiplierOfEm;
// properties and format runs
private ReadOnlySlice<char> _text;
private string _text;
private readonly SpanVector _formatRuns = new SpanVector(null);
private SpanPosition _latestPosition;
@ -69,9 +67,7 @@ namespace Avalonia.Media
ValidateFontSize(emSize);
_text = textToFormat != null ?
new ReadOnlySlice<char>(textToFormat.AsMemory()) :
throw new ArgumentNullException(nameof(textToFormat));
_text = textToFormat;
var runProps = new GenericTextRunProperties(
typeface,

2
src/Avalonia.Base/Media/GlyphMetrics.cs

@ -1,6 +1,6 @@
namespace Avalonia.Media;
public readonly struct GlyphMetrics
public readonly record struct GlyphMetrics
{
/// <summary>
/// Distance from the x-origin to the left extremum of the glyph.

419
src/Avalonia.Base/Media/GlyphRun.cs

@ -1,6 +1,5 @@
using System;
using System.Collections.Generic;
using System.Drawing;
using Avalonia.Media.TextFormatting.Unicode;
using Avalonia.Platform;
using Avalonia.Utilities;
@ -22,15 +21,12 @@ namespace Avalonia.Media
private Point? _baselineOrigin;
private GlyphRunMetrics? _glyphRunMetrics;
private ReadOnlySlice<char> _characters;
private IReadOnlyList<char> _characters;
private IReadOnlyList<ushort> _glyphIndices;
private IReadOnlyList<double>? _glyphAdvances;
private IReadOnlyList<Vector>? _glyphOffsets;
private IReadOnlyList<int>? _glyphClusters;
private int _offsetToFirstCharacter;
/// <summary>
/// Initializes a new instance of the <see cref="GlyphRun"/> class by specifying properties of the class.
/// </summary>
@ -45,7 +41,7 @@ namespace Avalonia.Media
public GlyphRun(
IGlyphTypeface glyphTypeface,
double fontRenderingEmSize,
ReadOnlySlice<char> characters,
IReadOnlyList<char> characters,
IReadOnlyList<ushort> glyphIndices,
IReadOnlyList<double>? glyphAdvances = null,
IReadOnlyList<Vector>? glyphOffsets = null,
@ -54,19 +50,19 @@ namespace Avalonia.Media
{
_glyphTypeface = glyphTypeface;
FontRenderingEmSize = fontRenderingEmSize;
_fontRenderingEmSize = fontRenderingEmSize;
Characters = characters;
_characters = characters;
_glyphIndices = glyphIndices;
GlyphAdvances = glyphAdvances;
_glyphAdvances = glyphAdvances;
GlyphOffsets = glyphOffsets;
_glyphOffsets = glyphOffsets;
GlyphClusters = glyphClusters;
_glyphClusters = glyphClusters;
BiDiLevel = biDiLevel;
_biDiLevel = biDiLevel;
}
/// <summary>
@ -145,7 +141,7 @@ namespace Avalonia.Media
/// <summary>
/// Gets or sets the list of UTF16 code points that represent the Unicode content of the <see cref="GlyphRun"/>.
/// </summary>
public ReadOnlySlice<char> Characters
public IReadOnlyList<char> Characters
{
get => _characters;
set => Set(ref _characters, value);
@ -219,7 +215,7 @@ namespace Avalonia.Media
/// </returns>
public double GetDistanceFromCharacterHit(CharacterHit characterHit)
{
var characterIndex = characterHit.FirstCharacterIndex + characterHit.TrailingLength - _offsetToFirstCharacter;
var characterIndex = characterHit.FirstCharacterIndex + characterHit.TrailingLength;
var distance = 0.0;
@ -227,12 +223,12 @@ namespace Avalonia.Media
{
if (GlyphClusters != null)
{
if (characterIndex < GlyphClusters[0])
if (characterIndex < Metrics.FirstCluster)
{
return 0;
}
if (characterIndex > GlyphClusters[GlyphClusters.Count - 1])
if (characterIndex > Metrics.LastCluster)
{
return Metrics.WidthIncludingTrailingWhitespace;
}
@ -268,12 +264,12 @@ namespace Avalonia.Media
if (GlyphClusters != null && GlyphClusters.Count > 0)
{
if (characterIndex > GlyphClusters[0])
if (characterIndex > Metrics.LastCluster)
{
return 0;
}
if (characterIndex <= GlyphClusters[GlyphClusters.Count - 1])
if (characterIndex <= Metrics.FirstCluster)
{
return Size.Width;
}
@ -299,19 +295,12 @@ namespace Avalonia.Media
/// </returns>
public CharacterHit GetCharacterHitFromDistance(double distance, out bool isInside)
{
var characterIndex = 0;
// Before
if (distance <= 0)
{
isInside = false;
if (GlyphClusters != null)
{
characterIndex = GlyphClusters[characterIndex];
}
var firstCharacterHit = FindNearestCharacterHit(characterIndex, out _);
var firstCharacterHit = FindNearestCharacterHit(IsLeftToRight ? Metrics.FirstCluster : Metrics.LastCluster, out _);
return IsLeftToRight ? new CharacterHit(firstCharacterHit.FirstCharacterIndex) : firstCharacterHit;
}
@ -321,18 +310,13 @@ namespace Avalonia.Media
{
isInside = false;
characterIndex = GlyphIndices.Count - 1;
if (GlyphClusters != null)
{
characterIndex = GlyphClusters[characterIndex];
}
var lastCharacterHit = FindNearestCharacterHit(characterIndex, out _);
var lastCharacterHit = FindNearestCharacterHit(IsLeftToRight ? Metrics.LastCluster : Metrics.FirstCluster, out _);
return IsLeftToRight ? lastCharacterHit : new CharacterHit(lastCharacterHit.FirstCharacterIndex);
}
var characterIndex = 0;
//Within
var currentX = 0d;
@ -378,7 +362,7 @@ namespace Avalonia.Media
var characterHit = FindNearestCharacterHit(characterIndex, out var width);
var delta = width / 2;
var offset = IsLeftToRight ? Math.Round(distance - currentX, 3) : Math.Round(currentX - distance, 3);
var isTrailing = offset > delta;
@ -400,24 +384,15 @@ namespace Avalonia.Media
{
characterHit = FindNearestCharacterHit(characterHit.FirstCharacterIndex, out _);
var textPosition = characterHit.FirstCharacterIndex + characterHit.TrailingLength;
return textPosition > _characters.End ?
characterHit :
new CharacterHit(characterHit.FirstCharacterIndex + characterHit.TrailingLength);
}
var nextCharacterHit =
FindNearestCharacterHit(characterHit.FirstCharacterIndex + characterHit.TrailingLength, out _);
if (characterHit.FirstCharacterIndex == Metrics.LastCluster)
{
return characterHit;
}
if (characterHit == nextCharacterHit)
{
return characterHit;
return new CharacterHit(characterHit.FirstCharacterIndex + characterHit.TrailingLength);
}
return characterHit.TrailingLength > 0 ?
nextCharacterHit :
new CharacterHit(nextCharacterHit.FirstCharacterIndex);
return FindNearestCharacterHit(characterHit.FirstCharacterIndex + characterHit.TrailingLength, out _);
}
/// <summary>
@ -454,29 +429,24 @@ namespace Avalonia.Media
return characterIndex;
}
if (IsLeftToRight)
if (characterIndex > Metrics.LastCluster)
{
if (characterIndex < GlyphClusters[0])
if (IsLeftToRight)
{
return 0;
return GlyphIndices.Count - 1;
}
if (characterIndex > GlyphClusters[GlyphClusters.Count - 1])
{
return GlyphClusters.Count - 1;
}
return 0;
}
else
{
if (characterIndex < GlyphClusters[GlyphClusters.Count - 1])
{
return GlyphClusters.Count - 1;
}
if (characterIndex > GlyphClusters[0])
if (characterIndex < Metrics.FirstCluster)
{
if (IsLeftToRight)
{
return 0;
}
return GlyphIndices.Count - 1;
}
var comparer = IsLeftToRight ? s_ascendingComparer : s_descendingComparer;
@ -498,7 +468,7 @@ namespace Avalonia.Media
if (start < 0)
{
return -1;
goto result;
}
}
@ -517,6 +487,18 @@ namespace Avalonia.Media
}
}
result:
if (start < 0)
{
return 0;
}
if (start > GlyphIndices.Count - 1)
{
return GlyphIndices.Count - 1;
}
return start;
}
@ -532,20 +514,20 @@ namespace Avalonia.Media
{
width = 0.0;
var start = FindGlyphIndex(index);
var glyphIndex = FindGlyphIndex(index);
if (GlyphClusters == null)
{
width = GetGlyphAdvance(index, out _);
return new CharacterHit(start, 1);
return new CharacterHit(glyphIndex, 1);
}
var cluster = GlyphClusters[start];
var cluster = GlyphClusters[glyphIndex];
var nextCluster = cluster;
var currentIndex = start;
var currentIndex = glyphIndex;
while (nextCluster == cluster)
{
@ -571,20 +553,64 @@ namespace Avalonia.Media
}
nextCluster = GlyphClusters[currentIndex];
}
}
int trailingLength;
var clusterLength = Math.Max(0, nextCluster - cluster);
if (nextCluster == cluster)
{
trailingLength = Characters.Start + Characters.Length - _offsetToFirstCharacter - cluster;
}
else
if (cluster == Metrics.LastCluster && clusterLength == 0)
{
trailingLength = nextCluster - cluster;
var characterLength = 0;
var currentCluster = Metrics.FirstCluster;
if (IsLeftToRight)
{
for (int i = 1; i < GlyphClusters.Count; i++)
{
nextCluster = GlyphClusters[i];
if (currentCluster > cluster)
{
break;
}
var length = nextCluster - currentCluster;
characterLength += length;
currentCluster = nextCluster;
}
}
else
{
for (int i = GlyphClusters.Count - 1; i >= 0; i--)
{
nextCluster = GlyphClusters[i];
if (currentCluster > cluster)
{
break;
}
var length = nextCluster - currentCluster;
characterLength += length;
currentCluster = nextCluster;
}
}
if (Characters != null)
{
clusterLength = Characters.Count - characterLength;
}
else
{
clusterLength = 1;
}
}
return new CharacterHit(_offsetToFirstCharacter + cluster, trailingLength);
return new CharacterHit(cluster, clusterLength);
}
/// <summary>
@ -618,22 +644,25 @@ namespace Avalonia.Media
private GlyphRunMetrics CreateGlyphRunMetrics()
{
var firstCluster = 0;
var lastCluster = Characters.Length - 1;
int firstCluster = 0, lastCluster = 0;
if (!IsLeftToRight)
if (_glyphClusters != null && _glyphClusters.Count > 0)
{
var cluster = firstCluster;
firstCluster = lastCluster;
lastCluster = cluster;
firstCluster = _glyphClusters[0];
lastCluster = _glyphClusters[_glyphClusters.Count - 1];
}
if (GlyphClusters != null && GlyphClusters.Count > 0)
else
{
firstCluster = GlyphClusters[0];
lastCluster = GlyphClusters[GlyphClusters.Count - 1];
if (Characters != null && Characters.Count > 0)
{
firstCluster = 0;
lastCluster = Characters.Count - 1;
}
}
_offsetToFirstCharacter = Math.Max(0, Characters.Start - firstCluster);
if (!IsLeftToRight)
{
(lastCluster, firstCluster) = (firstCluster, lastCluster);
}
var isReversed = firstCluster > lastCluster;
@ -666,12 +695,19 @@ namespace Avalonia.Media
}
}
return new GlyphRunMetrics(width, widthIncludingTrailingWhitespace, trailingWhitespaceLength, newLineLength,
height);
return new GlyphRunMetrics(
width,
widthIncludingTrailingWhitespace,
height,
trailingWhitespaceLength,
newLineLength,
firstCluster,
lastCluster
);
}
private int GetTrailingWhitespaceLength(bool isReversed, out int newLineLength, out int glyphCount)
{
{
if (isReversed)
{
return GetTralingWhitespaceLengthRightToLeft(out newLineLength, out glyphCount);
@ -681,66 +717,82 @@ namespace Avalonia.Media
newLineLength = 0;
var trailingWhitespaceLength = 0;
if (GlyphClusters == null)
if (Characters != null)
{
for (var i = _characters.Length - 1; i >= 0;)
if (GlyphClusters == null)
{
var codepoint = Codepoint.ReadAt(_characters, i, out var count);
if (!codepoint.IsWhiteSpace)
for (var i = _characters.Count - 1; i >= 0;)
{
break;
}
var codepoint = Codepoint.ReadAt(_characters, i, out var count);
if (codepoint.IsBreakChar)
{
newLineLength++;
}
if (!codepoint.IsWhiteSpace)
{
break;
}
trailingWhitespaceLength++;
if (codepoint.IsBreakChar)
{
newLineLength++;
}
trailingWhitespaceLength++;
i -= count;
glyphCount++;
i -= count;
glyphCount++;
}
}
}
else
{
for (var i = GlyphClusters.Count - 1; i >= 0; i--)
else
{
var currentCluster = GlyphClusters[i];
var characterIndex = Math.Max(0, currentCluster - _characters.BufferOffset);
var codepoint = Codepoint.ReadAt(_characters, characterIndex, out _);
if (!codepoint.IsWhiteSpace)
if (Characters.Count > 0)
{
break;
}
var characterIndex = Characters.Count - 1;
var clusterLength = 1;
for (var i = GlyphClusters.Count - 1; i >= 0; i--)
{
var currentCluster = GlyphClusters[i];
var codepoint = Codepoint.ReadAt(_characters, characterIndex, out var characterLength);
while(i - 1 >= 0)
{
var nextCluster = GlyphClusters[i - 1];
characterIndex -= characterLength;
if(currentCluster == nextCluster)
{
clusterLength++;
i--;
if (!codepoint.IsWhiteSpace)
{
break;
}
continue;
}
var clusterLength = 1;
break;
}
while (i - 1 >= 0)
{
var nextCluster = GlyphClusters[i - 1];
if (codepoint.IsBreakChar)
{
newLineLength += clusterLength;
}
if (currentCluster == nextCluster)
{
clusterLength++;
i--;
if(characterIndex >= 0)
{
codepoint = Codepoint.ReadAt(_characters, characterIndex, out characterLength);
characterIndex -= characterLength;
}
continue;
}
break;
}
if (codepoint.IsBreakChar)
{
newLineLength += clusterLength;
}
trailingWhitespaceLength += clusterLength;
glyphCount++;
trailingWhitespaceLength += clusterLength;
glyphCount++;
}
}
}
}
@ -753,67 +805,73 @@ namespace Avalonia.Media
newLineLength = 0;
var trailingWhitespaceLength = 0;
if (GlyphClusters == null)
if (Characters != null)
{
for (var i = 0; i < Characters.Length;)
if (GlyphClusters == null)
{
var codepoint = Codepoint.ReadAt(_characters, i, out var count);
if (!codepoint.IsWhiteSpace)
for (var i = 0; i < Characters.Count;)
{
break;
}
var codepoint = Codepoint.ReadAt(_characters, i, out var count);
if (codepoint.IsBreakChar)
{
newLineLength++;
}
if (!codepoint.IsWhiteSpace)
{
break;
}
trailingWhitespaceLength++;
if (codepoint.IsBreakChar)
{
newLineLength++;
}
i += count;
glyphCount++;
trailingWhitespaceLength++;
i += count;
glyphCount++;
}
}
}
else
{
for (var i = 0; i < GlyphClusters.Count; i++)
else
{
var currentCluster = GlyphClusters[i];
var characterIndex = Math.Max(0, currentCluster - _characters.BufferOffset);
var codepoint = Codepoint.ReadAt(_characters, characterIndex, out _);
var characterIndex = 0;
if (!codepoint.IsWhiteSpace)
for (var i = 0; i < GlyphClusters.Count; i++)
{
break;
}
var currentCluster = GlyphClusters[i];
var codepoint = Codepoint.ReadAt(_characters, characterIndex, out var characterLength);
var clusterLength = 1;
characterIndex += characterLength;
var j = i;
if (!codepoint.IsWhiteSpace)
{
break;
}
while (j - 1 >= 0)
{
var nextCluster = GlyphClusters[--j];
var clusterLength = 1;
if (currentCluster == nextCluster)
var j = i;
while (j - 1 >= 0)
{
clusterLength++;
var nextCluster = GlyphClusters[--j];
continue;
}
if (currentCluster == nextCluster)
{
clusterLength++;
break;
}
continue;
}
if (codepoint.IsBreakChar)
{
newLineLength += clusterLength;
}
break;
}
if (codepoint.IsBreakChar)
{
newLineLength += clusterLength;
}
trailingWhitespaceLength += clusterLength;
trailingWhitespaceLength += clusterLength;
glyphCount += clusterLength;
glyphCount += clusterLength;
}
}
}
@ -855,14 +913,9 @@ namespace Avalonia.Media
throw new InvalidOperationException();
}
_glyphRunImpl = CreateGlyphRunImpl();
}
private IGlyphRunImpl CreateGlyphRunImpl()
{
var platformRenderInterface = AvaloniaLocator.Current.GetRequiredService<IPlatformRenderInterface>();
return platformRenderInterface.CreateGlyphRun(GlyphTypeface, FontRenderingEmSize, GlyphIndices, GlyphAdvances, GlyphOffsets);
_glyphRunImpl = platformRenderInterface.CreateGlyphRun(GlyphTypeface, FontRenderingEmSize, GlyphIndices, GlyphAdvances, GlyphOffsets);
}
void IDisposable.Dispose()

20
src/Avalonia.Base/Media/GlyphRunMetrics.cs

@ -1,25 +1,31 @@
namespace Avalonia.Media
{
public readonly struct GlyphRunMetrics
public readonly record struct GlyphRunMetrics
{
public GlyphRunMetrics(double width, double widthIncludingTrailingWhitespace, int trailingWhitespaceLength,
int newlineLength, double height)
public GlyphRunMetrics(double width, double widthIncludingTrailingWhitespace, double height,
int trailingWhitespaceLength, int newLineLength, int firstCluster, int lastCluster)
{
Width = width;
WidthIncludingTrailingWhitespace = widthIncludingTrailingWhitespace;
TrailingWhitespaceLength = trailingWhitespaceLength;
NewlineLength = newlineLength;
Height = height;
TrailingWhitespaceLength = trailingWhitespaceLength;
NewLineLength= newLineLength;
FirstCluster = firstCluster;
LastCluster = lastCluster;
}
public double Width { get; }
public double WidthIncludingTrailingWhitespace { get; }
public double Height { get; }
public int TrailingWhitespaceLength { get; }
public int NewlineLength { get; }
public int NewLineLength { get; }
public double Height { get; }
public int FirstCluster { get; }
public int LastCluster { get; }
}
}

2
src/Avalonia.Base/Media/Imaging/RenderTargetBitmap.cs

@ -60,5 +60,7 @@ namespace Avalonia.Media.Imaging
/// <inheritdoc/>
public IDrawingContextImpl CreateDrawingContext(IVisualBrushRenderer? vbr) => PlatformImpl.Item.CreateDrawingContext(vbr);
bool IRenderTarget.IsCorrupted => false;
}
}

2
src/Avalonia.Base/Media/TextCollapsingCreateInfo.cs

@ -2,7 +2,7 @@
namespace Avalonia.Media
{
public readonly struct TextCollapsingCreateInfo
public readonly record struct TextCollapsingCreateInfo
{
public readonly double Width;
public readonly TextRunProperties TextRunProperties;

293
src/Avalonia.Base/Media/TextFormatting/CharacterBufferRange.cs

@ -0,0 +1,293 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using Avalonia.Utilities;
namespace Avalonia.Media.TextFormatting
{
public readonly struct CharacterBufferRange : IReadOnlyList<char>
{
/// <summary>
/// Getting an empty character string
/// </summary>
public static CharacterBufferRange Empty => new CharacterBufferRange();
/// <summary>
/// Construct <see cref="CharacterBufferRange"/> from character array
/// </summary>
/// <param name="characterArray">character array</param>
/// <param name="offsetToFirstChar">character buffer offset to the first character</param>
/// <param name="characterLength">character length</param>
public CharacterBufferRange(
char[] characterArray,
int offsetToFirstChar,
int characterLength
)
: this(
new CharacterBufferReference(characterArray, offsetToFirstChar),
characterLength
)
{ }
/// <summary>
/// Construct <see cref="CharacterBufferRange"/> from string
/// </summary>
/// <param name="characterString">character string</param>
/// <param name="offsetToFirstChar">character buffer offset to the first character</param>
/// <param name="characterLength">character length</param>
public CharacterBufferRange(
string characterString,
int offsetToFirstChar,
int characterLength
)
: this(
new CharacterBufferReference(characterString, offsetToFirstChar),
characterLength
)
{ }
/// <summary>
/// Construct a <see cref="CharacterBufferRange"/> from <see cref="CharacterBufferReference"/>
/// </summary>
/// <param name="characterBufferReference">character buffer reference</param>
/// <param name="characterLength">number of characters</param>
public CharacterBufferRange(
CharacterBufferReference characterBufferReference,
int characterLength
)
{
if (characterLength < 0)
{
throw new ArgumentOutOfRangeException("characterLength", "ParameterCannotBeNegative");
}
int maxLength = characterBufferReference.CharacterBuffer.Length > 0 ?
characterBufferReference.CharacterBuffer.Length - characterBufferReference.OffsetToFirstChar :
0;
if (characterLength > maxLength)
{
throw new ArgumentOutOfRangeException("characterLength", $"ParameterCannotBeGreaterThan {maxLength}");
}
CharacterBufferReference = characterBufferReference;
Length = characterLength;
}
/// <summary>
/// Construct a <see cref="CharacterBufferRange"/> from part of another <see cref="CharacterBufferRange"/>
/// </summary>
internal CharacterBufferRange(
CharacterBufferRange characterBufferRange,
int offsetToFirstChar,
int characterLength
) :
this(
characterBufferRange.CharacterBuffer,
characterBufferRange.OffsetToFirstChar + offsetToFirstChar,
characterLength
)
{ }
/// <summary>
/// Construct a <see cref="CharacterBufferRange"/> from string
/// </summary>
internal CharacterBufferRange(
string charString
) :
this(
charString,
0,
charString.Length
)
{ }
/// <summary>
/// Construct <see cref="CharacterBufferRange"/> from memory buffer
/// </summary>
internal CharacterBufferRange(
ReadOnlyMemory<char> charBuffer,
int offsetToFirstChar,
int characterLength
) :
this(
new CharacterBufferReference(charBuffer, offsetToFirstChar),
characterLength
)
{ }
/// <summary>
/// Construct a <see cref="CharacterBufferRange"/> by extracting text info from a text run
/// </summary>
internal CharacterBufferRange(TextRun textRun)
{
CharacterBufferReference = textRun.CharacterBufferReference;
Length = textRun.Length;
}
public char this[int index]
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get
{
#if DEBUG
if (index.CompareTo(0) < 0 || index.CompareTo(Length) > 0)
{
throw new ArgumentOutOfRangeException(nameof(index));
}
#endif
return Span[index];
}
}
/// <summary>
/// Gets a reference to the character buffer
/// </summary>
public CharacterBufferReference CharacterBufferReference { get; }
/// <summary>
/// Gets the number of characters in text source character store
/// </summary>
public int Length { get; }
/// <summary>
/// Gets a span from the character buffer range
/// </summary>
public ReadOnlySpan<char> Span =>
CharacterBufferReference.CharacterBuffer.Span.Slice(CharacterBufferReference.OffsetToFirstChar, Length);
/// <summary>
/// Gets the character memory buffer
/// </summary>
internal ReadOnlyMemory<char> CharacterBuffer
{
get { return CharacterBufferReference.CharacterBuffer; }
}
/// <summary>
/// Gets the character offset relative to the beginning of buffer to
/// the first character of the run
/// </summary>
internal int OffsetToFirstChar
{
get { return CharacterBufferReference.OffsetToFirstChar; }
}
/// <summary>
/// Indicate whether the character buffer range is empty
/// </summary>
internal bool IsEmpty
{
get { return CharacterBufferReference.CharacterBuffer.Length == 0 || Length <= 0; }
}
internal CharacterBufferRange Take(int length)
{
if (IsEmpty)
{
return this;
}
if (length > Length)
{
throw new ArgumentOutOfRangeException(nameof(length));
}
return new CharacterBufferRange(CharacterBufferReference, length);
}
internal CharacterBufferRange Skip(int length)
{
if (IsEmpty)
{
return this;
}
if (length > Length)
{
throw new ArgumentOutOfRangeException(nameof(length));
}
if (length == Length)
{
return new CharacterBufferRange(new CharacterBufferReference(), 0);
}
var characterBufferReference = new CharacterBufferReference(
CharacterBufferReference.CharacterBuffer,
CharacterBufferReference.OffsetToFirstChar + length);
return new CharacterBufferRange(characterBufferReference, Length - length);
}
/// <summary>
/// Compute hash code
/// </summary>
public override int GetHashCode()
{
return CharacterBufferReference.GetHashCode() ^ Length;
}
/// <summary>
/// Test equality with the input object
/// </summary>
/// <param name="obj"> The object to test </param>
public override bool Equals(object? obj)
{
if (obj is CharacterBufferRange range)
{
return Equals(range);
}
return false;
}
/// <summary>
/// Test equality with the input CharacterBufferRange
/// </summary>
/// <param name="value"> The CharacterBufferRange value to test </param>
public bool Equals(CharacterBufferRange value)
{
return CharacterBufferReference.Equals(value.CharacterBufferReference)
&& Length == value.Length;
}
/// <summary>
/// Compare two CharacterBufferRange for equality
/// </summary>
/// <param name="left">left operand</param>
/// <param name="right">right operand</param>
/// <returns>whether or not two operands are equal</returns>
public static bool operator ==(CharacterBufferRange left, CharacterBufferRange right)
{
return left.Equals(right);
}
/// <summary>
/// Compare two CharacterBufferRange for inequality
/// </summary>
/// <param name="left">left operand</param>
/// <param name="right">right operand</param>
/// <returns>whether or not two operands are equal</returns>
public static bool operator !=(CharacterBufferRange left, CharacterBufferRange right)
{
return !(left == right);
}
int IReadOnlyCollection<char>.Count => Length;
public IEnumerator<char> GetEnumerator()
{
return new ImmutableReadOnlyListStructEnumerator<char>(this);
}
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
}
}

115
src/Avalonia.Base/Media/TextFormatting/CharacterBufferReference.cs

@ -0,0 +1,115 @@
using System;
namespace Avalonia.Media.TextFormatting
{
/// <summary>
/// Text character buffer reference
/// </summary>
public readonly struct CharacterBufferReference : IEquatable<CharacterBufferReference>
{
/// <summary>
/// Construct character buffer reference from character array
/// </summary>
/// <param name="characterArray">character array</param>
/// <param name="offsetToFirstChar">character buffer offset to the first character</param>
public CharacterBufferReference(char[] characterArray, int offsetToFirstChar = 0)
: this(characterArray.AsMemory(), offsetToFirstChar)
{ }
/// <summary>
/// Construct character buffer reference from string
/// </summary>
/// <param name="characterString">character string</param>
/// <param name="offsetToFirstChar">character buffer offset to the first character</param>
public CharacterBufferReference(string characterString, int offsetToFirstChar = 0)
: this(characterString.AsMemory(), offsetToFirstChar)
{ }
/// <summary>
/// Construct character buffer reference from memory buffer
/// </summary>
internal CharacterBufferReference(ReadOnlyMemory<char> characterBuffer, int offsetToFirstChar = 0)
{
if (offsetToFirstChar < 0)
{
throw new ArgumentOutOfRangeException("offsetToFirstChar", "ParameterCannotBeNegative");
}
// maximum offset is one less than CharacterBuffer.Count, except that zero is always a valid offset
// even in the case of an empty or null character buffer
var maxOffset = characterBuffer.Length == 0 ? 0 : Math.Max(0, characterBuffer.Length - 1);
if (offsetToFirstChar > maxOffset)
{
throw new ArgumentOutOfRangeException("offsetToFirstChar", $"ParameterCannotBeGreaterThan, {maxOffset}");
}
CharacterBuffer = characterBuffer;
OffsetToFirstChar = offsetToFirstChar;
}
/// <summary>
/// Gets the character memory buffer
/// </summary>
public ReadOnlyMemory<char> CharacterBuffer { get; }
/// <summary>
/// Gets the character offset relative to the beginning of buffer to
/// the first character of the run
/// </summary>
public int OffsetToFirstChar { get; }
/// <summary>
/// Compute hash code
/// </summary>
public override int GetHashCode()
{
return CharacterBuffer.IsEmpty ? 0 : CharacterBuffer.GetHashCode();
}
/// <summary>
/// Test equality with the input object
/// </summary>
/// <param name="obj"> The object to test. </param>
public override bool Equals(object? obj)
{
if (obj is CharacterBufferReference reference)
{
return Equals(reference);
}
return false;
}
/// <summary>
/// Test equality with the input CharacterBufferReference
/// </summary>
/// <param name="value"> The characterBufferReference value to test </param>
public bool Equals(CharacterBufferReference value)
{
return CharacterBuffer.Equals(value.CharacterBuffer);
}
/// <summary>
/// Compare two CharacterBufferReference for equality
/// </summary>
/// <param name="left">left operand</param>
/// <param name="right">right operand</param>
/// <returns>whether or not two operands are equal</returns>
public static bool operator ==(CharacterBufferReference left, CharacterBufferReference right)
{
return left.Equals(right);
}
/// <summary>
/// Compare two CharacterBufferReference for inequality
/// </summary>
/// <param name="left">left operand</param>
/// <param name="right">right operand</param>
/// <returns>whether or not two operands are equal</returns>
public static bool operator !=(CharacterBufferReference left, CharacterBufferReference right)
{
return !(left == right);
}
}
}

13
src/Avalonia.Base/Media/TextFormatting/FormattedTextSource.cs

@ -7,14 +7,15 @@ namespace Avalonia.Media.TextFormatting
{
internal readonly struct FormattedTextSource : ITextSource
{
private readonly ReadOnlySlice<char> _text;
private readonly CharacterBufferRange _text;
private readonly int length;
private readonly TextRunProperties _defaultProperties;
private readonly IReadOnlyList<ValueSpan<TextRunProperties>>? _textModifier;
public FormattedTextSource(ReadOnlySlice<char> text, TextRunProperties defaultProperties,
public FormattedTextSource(string text, TextRunProperties defaultProperties,
IReadOnlyList<ValueSpan<TextRunProperties>>? textModifier)
{
_text = text;
_text = new CharacterBufferRange(text);
_defaultProperties = defaultProperties;
_textModifier = textModifier;
}
@ -35,7 +36,7 @@ namespace Avalonia.Media.TextFormatting
var textStyleRun = CreateTextStyleRun(runText, textSourceIndex, _defaultProperties, _textModifier);
return new TextCharacters(runText.Take(textStyleRun.Length), textStyleRun.Value);
return new TextCharacters(runText.Take(textStyleRun.Length).CharacterBufferReference, textStyleRun.Length, textStyleRun.Value);
}
/// <summary>
@ -48,7 +49,7 @@ namespace Avalonia.Media.TextFormatting
/// <returns>
/// The created text style run.
/// </returns>
private static ValueSpan<TextRunProperties> CreateTextStyleRun(ReadOnlySlice<char> text, int firstTextSourceIndex,
private static ValueSpan<TextRunProperties> CreateTextStyleRun(CharacterBufferRange text, int firstTextSourceIndex,
TextRunProperties defaultProperties, IReadOnlyList<ValueSpan<TextRunProperties>>? textModifier)
{
if (textModifier == null || textModifier.Count == 0)
@ -122,7 +123,7 @@ namespace Avalonia.Media.TextFormatting
return new ValueSpan<TextRunProperties>(firstTextSourceIndex, length, currentProperties);
}
private static int CoerceLength(ReadOnlySlice<char> text, int length)
private static int CoerceLength(CharacterBufferRange text, int length)
{
var finalLength = 0;

19
src/Avalonia.Base/Media/TextFormatting/InterWordJustification.cs

@ -46,28 +46,30 @@ namespace Avalonia.Media.TextFormatting
var breakOportunities = new Queue<int>();
var currentPosition = textLine.FirstTextSourceIndex;
foreach (var textRun in lineImpl.TextRuns)
{
var text = textRun.Text;
var text = new CharacterBufferRange(textRun);
if (text.IsEmpty)
{
continue;
}
var start = text.Start;
var lineBreakEnumerator = new LineBreakEnumerator(text);
while (lineBreakEnumerator.MoveNext())
{
var currentBreak = lineBreakEnumerator.Current;
if (!currentBreak.Required && currentBreak.PositionWrap != text.Length)
if (!currentBreak.Required && currentBreak.PositionWrap != textRun.Length)
{
breakOportunities.Enqueue(start + currentBreak.PositionMeasure);
breakOportunities.Enqueue(currentPosition + currentBreak.PositionMeasure);
}
}
currentPosition += textRun.Length;
}
if (breakOportunities.Count == 0)
@ -78,9 +80,11 @@ namespace Avalonia.Media.TextFormatting
var remainingSpace = Math.Max(0, paragraphWidth - lineImpl.WidthIncludingTrailingWhitespace);
var spacing = remainingSpace / breakOportunities.Count;
currentPosition = textLine.FirstTextSourceIndex;
foreach (var textRun in lineImpl.TextRuns)
{
var text = textRun.Text;
var text = textRun.CharacterBufferReference.CharacterBuffer;
if (text.IsEmpty)
{
@ -91,7 +95,6 @@ namespace Avalonia.Media.TextFormatting
{
var glyphRun = shapedText.GlyphRun;
var shapedBuffer = shapedText.ShapedBuffer;
var currentPosition = text.Start;
while (breakOportunities.Count > 0)
{
@ -110,6 +113,8 @@ namespace Avalonia.Media.TextFormatting
glyphRun.GlyphAdvances = shapedBuffer.GlyphAdvances;
}
currentPosition += textRun.Length;
}
}
}

20
src/Avalonia.Base/Media/TextFormatting/ShapeableTextCharacters.cs

@ -7,30 +7,26 @@ namespace Avalonia.Media.TextFormatting
/// </summary>
public sealed class ShapeableTextCharacters : TextRun
{
public ShapeableTextCharacters(ReadOnlySlice<char> text, TextRunProperties properties, sbyte biDiLevel)
public ShapeableTextCharacters(CharacterBufferReference characterBufferReference, int length,
TextRunProperties properties, sbyte biDiLevel)
{
TextSourceLength = text.Length;
Text = text;
CharacterBufferReference = characterBufferReference;
Length = length;
Properties = properties;
BidiLevel = biDiLevel;
}
public override int TextSourceLength { get; }
public override int Length { get; }
public override ReadOnlySlice<char> Text { get; }
public override CharacterBufferReference CharacterBufferReference { get; }
public override TextRunProperties Properties { get; }
public sbyte BidiLevel { get; }
public bool CanShapeTogether(ShapeableTextCharacters shapeableTextCharacters)
{
if (!Text.Buffer.Equals(shapeableTextCharacters.Text.Buffer))
{
return false;
}
if (Text.Start + Text.Length != shapeableTextCharacters.Text.Start)
if (!CharacterBufferReference.Equals(shapeableTextCharacters.CharacterBufferReference))
{
return false;
}

33
src/Avalonia.Base/Media/TextFormatting/ShapedBuffer.cs

@ -7,16 +7,16 @@ namespace Avalonia.Media.TextFormatting
public sealed class ShapedBuffer : IList<GlyphInfo>
{
private static readonly IComparer<GlyphInfo> s_clusterComparer = new CompareClusters();
public ShapedBuffer(ReadOnlySlice<char> text, int length, IGlyphTypeface glyphTypeface, double fontRenderingEmSize, sbyte bidiLevel)
: this(text, new GlyphInfo[length], glyphTypeface, fontRenderingEmSize, bidiLevel)
public ShapedBuffer(CharacterBufferRange characterBufferRange, int bufferLength, IGlyphTypeface glyphTypeface, double fontRenderingEmSize, sbyte bidiLevel) :
this(characterBufferRange, new GlyphInfo[bufferLength], glyphTypeface, fontRenderingEmSize, bidiLevel)
{
}
internal ShapedBuffer(ReadOnlySlice<char> text, ArraySlice<GlyphInfo> glyphInfos, IGlyphTypeface glyphTypeface, double fontRenderingEmSize, sbyte bidiLevel)
internal ShapedBuffer(CharacterBufferRange characterBufferRange, ArraySlice<GlyphInfo> glyphInfos, IGlyphTypeface glyphTypeface, double fontRenderingEmSize, sbyte bidiLevel)
{
Text = text;
CharacterBufferRange = characterBufferRange;
GlyphInfos = glyphInfos;
GlyphTypeface = glyphTypeface;
FontRenderingEmSize = fontRenderingEmSize;
@ -24,9 +24,7 @@ namespace Avalonia.Media.TextFormatting
}
internal ArraySlice<GlyphInfo> GlyphInfos { get; }
public ReadOnlySlice<char> Text { get; }
public int Length => GlyphInfos.Length;
public IGlyphTypeface GlyphTypeface { get; }
@ -45,6 +43,8 @@ namespace Avalonia.Media.TextFormatting
public IReadOnlyList<Vector> GlyphOffsets => new GlyphOffsetList(GlyphInfos);
public CharacterBufferRange CharacterBufferRange { get; }
/// <summary>
/// Finds a glyph index for given character index.
/// </summary>
@ -105,16 +105,23 @@ namespace Avalonia.Media.TextFormatting
/// <returns>The split result.</returns>
internal SplitResult<ShapedBuffer> Split(int length)
{
if (Text.Length == length)
if (CharacterBufferRange.Length == length)
{
return new SplitResult<ShapedBuffer>(this, null);
}
var glyphCount = FindGlyphIndex(Text.Start + length);
var firstCluster = GlyphClusters[0];
var lastCluster = GlyphClusters[GlyphClusters.Count - 1];
var start = firstCluster < lastCluster ? firstCluster : lastCluster;
var glyphCount = FindGlyphIndex(start + length);
var first = new ShapedBuffer(Text.Take(length), GlyphInfos.Take(glyphCount), GlyphTypeface, FontRenderingEmSize, BidiLevel);
var first = new ShapedBuffer(CharacterBufferRange.Take(length),
GlyphInfos.Take(glyphCount), GlyphTypeface, FontRenderingEmSize, BidiLevel);
var second = new ShapedBuffer(Text.Skip(length), GlyphInfos.Skip(glyphCount), GlyphTypeface, FontRenderingEmSize, BidiLevel);
var second = new ShapedBuffer(CharacterBufferRange.Skip(length),
GlyphInfos.Skip(glyphCount), GlyphTypeface, FontRenderingEmSize, BidiLevel);
return new SplitResult<ShapedBuffer>(first, second);
}
@ -255,7 +262,7 @@ namespace Avalonia.Media.TextFormatting
}
}
public readonly struct GlyphInfo
public readonly record struct GlyphInfo
{
public GlyphInfo(ushort glyphIndex, int glyphCluster, double glyphAdvance = 0, Vector glyphOffset = default)
{

19
src/Avalonia.Base/Media/TextFormatting/ShapedTextCharacters.cs

@ -1,6 +1,5 @@
using System;
using Avalonia.Media.TextFormatting.Unicode;
using Avalonia.Utilities;
namespace Avalonia.Media.TextFormatting
{
@ -14,10 +13,10 @@ namespace Avalonia.Media.TextFormatting
public ShapedTextCharacters(ShapedBuffer shapedBuffer, TextRunProperties properties)
{
ShapedBuffer = shapedBuffer;
Text = shapedBuffer.Text;
CharacterBufferReference = shapedBuffer.CharacterBufferRange.CharacterBufferReference;
Length = shapedBuffer.CharacterBufferRange.Length;
Properties = properties;
TextSourceLength = Text.Length;
TextMetrics = new TextMetrics(properties.Typeface, properties.FontRenderingEmSize);
TextMetrics = new TextMetrics(properties.Typeface.GlyphTypeface, properties.FontRenderingEmSize);
}
public bool IsReversed { get; private set; }
@ -27,13 +26,13 @@ namespace Avalonia.Media.TextFormatting
public ShapedBuffer ShapedBuffer { get; }
/// <inheritdoc/>
public override ReadOnlySlice<char> Text { get; }
public override CharacterBufferReference CharacterBufferReference { get; }
/// <inheritdoc/>
public override TextRunProperties Properties { get; }
/// <inheritdoc/>
public override int TextSourceLength { get; }
public override int Length { get; }
public TextMetrics TextMetrics { get; }
@ -176,12 +175,12 @@ namespace Avalonia.Media.TextFormatting
#if DEBUG
if (first.Text.Length != length)
if (first.Length != length)
{
throw new InvalidOperationException("Split length mismatch.");
}
#endif
#endif
var second = new ShapedTextCharacters(splitBuffer.Second!, Properties);
@ -193,7 +192,7 @@ namespace Avalonia.Media.TextFormatting
return new GlyphRun(
ShapedBuffer.GlyphTypeface,
ShapedBuffer.FontRenderingEmSize,
Text,
new CharacterBufferRange(CharacterBufferReference, Length),
ShapedBuffer.GlyphIndices,
ShapedBuffer.GlyphAdvances,
ShapedBuffer.GlyphOffsets,

4
src/Avalonia.Base/Media/TextFormatting/SplitResult.cs

@ -1,6 +1,8 @@
namespace Avalonia.Media.TextFormatting
{
internal readonly struct SplitResult<T>
#pragma warning disable CA1815 // Override equals and operator equals on value types
public readonly struct SplitResult<T>
#pragma warning restore CA1815 // Override equals and operator equals on value types
{
public SplitResult(T first, T? second)
{

129
src/Avalonia.Base/Media/TextFormatting/TextCharacters.cs

@ -1,7 +1,6 @@
using System;
using System.Collections.Generic;
using Avalonia.Media.TextFormatting.Unicode;
using Avalonia.Utilities;
namespace Avalonia.Media.TextFormatting
{
@ -10,26 +9,83 @@ namespace Avalonia.Media.TextFormatting
/// </summary>
public class TextCharacters : TextRun
{
public TextCharacters(ReadOnlySlice<char> text, TextRunProperties properties)
{
TextSourceLength = text.Length;
Text = text;
Properties = properties;
}
/// <summary>
/// Construct a run of text content from character array
/// </summary>
public TextCharacters(
char[] characterArray,
int offsetToFirstChar,
int length,
TextRunProperties textRunProperties
) :
this(
new CharacterBufferReference(characterArray, offsetToFirstChar),
length,
textRunProperties
)
{ }
public TextCharacters(ReadOnlySlice<char> text, int offsetToFirstCharacter, int length,
TextRunProperties properties)
/// <summary>
/// Construct a run for text content from string
/// </summary>
public TextCharacters(
string characterString,
TextRunProperties textRunProperties
) :
this(
characterString,
0, // offsetToFirstChar
(characterString == null) ? 0 : characterString.Length,
textRunProperties
)
{ }
/// <summary>
/// Construct a run for text content from string
/// </summary>
public TextCharacters(
string characterString,
int offsetToFirstChar,
int length,
TextRunProperties textRunProperties
) :
this(
new CharacterBufferReference(characterString, offsetToFirstChar),
length,
textRunProperties
)
{ }
/// <summary>
/// Internal constructor of TextContent
/// </summary>
public TextCharacters(
CharacterBufferReference characterBufferReference,
int length,
TextRunProperties textRunProperties
)
{
Text = text.Skip(offsetToFirstCharacter).Take(length);
TextSourceLength = length;
Properties = properties;
if (length <= 0)
{
throw new ArgumentOutOfRangeException("length", "ParameterMustBeGreaterThanZero");
}
if (textRunProperties.FontRenderingEmSize <= 0)
{
throw new ArgumentOutOfRangeException("textRunProperties.FontRenderingEmSize", "ParameterMustBeGreaterThanZero");
}
CharacterBufferReference = characterBufferReference;
Length = length;
Properties = textRunProperties;
}
/// <inheritdoc />
public override int TextSourceLength { get; }
public override int Length { get; }
/// <inheritdoc />
public override ReadOnlySlice<char> Text { get; }
public override CharacterBufferReference CharacterBufferReference { get; }
/// <inheritdoc />
public override TextRunProperties Properties { get; }
@ -38,18 +94,17 @@ namespace Avalonia.Media.TextFormatting
/// Gets a list of <see cref="ShapeableTextCharacters"/>.
/// </summary>
/// <returns>The shapeable text characters.</returns>
internal IReadOnlyList<ShapeableTextCharacters> GetShapeableCharacters(ReadOnlySlice<char> runText, sbyte biDiLevel,
ref TextRunProperties? previousProperties)
internal IReadOnlyList<ShapeableTextCharacters> GetShapeableCharacters(CharacterBufferRange characterBufferRange, sbyte biDiLevel, ref TextRunProperties? previousProperties)
{
var shapeableCharacters = new List<ShapeableTextCharacters>(2);
while (!runText.IsEmpty)
while (characterBufferRange.Length > 0)
{
var shapeableRun = CreateShapeableRun(runText, Properties, biDiLevel, ref previousProperties);
var shapeableRun = CreateShapeableRun(characterBufferRange, Properties, biDiLevel, ref previousProperties);
shapeableCharacters.Add(shapeableRun);
runText = runText.Skip(shapeableRun.Text.Length);
characterBufferRange = characterBufferRange.Skip(shapeableRun.Length);
previousProperties = shapeableRun.Properties;
}
@ -60,45 +115,45 @@ namespace Avalonia.Media.TextFormatting
/// <summary>
/// Creates a shapeable text run with unique properties.
/// </summary>
/// <param name="text">The text to create text runs from.</param>
/// <param name="characterBufferRange">The character buffer range to create text runs from.</param>
/// <param name="defaultProperties">The default text run properties.</param>
/// <param name="biDiLevel">The bidi level of the run.</param>
/// <param name="previousProperties"></param>
/// <returns>A list of shapeable text runs.</returns>
private static ShapeableTextCharacters CreateShapeableRun(ReadOnlySlice<char> text,
private static ShapeableTextCharacters CreateShapeableRun(CharacterBufferRange characterBufferRange,
TextRunProperties defaultProperties, sbyte biDiLevel, ref TextRunProperties? previousProperties)
{
var defaultTypeface = defaultProperties.Typeface;
var currentTypeface = defaultTypeface;
var previousTypeface = previousProperties?.Typeface;
if (TryGetShapeableLength(text, currentTypeface, null, out var count, out var script))
if (TryGetShapeableLength(characterBufferRange, currentTypeface, null, out var count, out var script))
{
if (script == Script.Common && previousTypeface is not null)
{
if (TryGetShapeableLength(text, previousTypeface.Value, null, out var fallbackCount, out _))
if (TryGetShapeableLength(characterBufferRange, previousTypeface.Value, null, out var fallbackCount, out _))
{
return new ShapeableTextCharacters(text.Take(fallbackCount),
return new ShapeableTextCharacters(characterBufferRange.CharacterBufferReference, fallbackCount,
defaultProperties.WithTypeface(previousTypeface.Value), biDiLevel);
}
}
return new ShapeableTextCharacters(text.Take(count), defaultProperties.WithTypeface(currentTypeface),
return new ShapeableTextCharacters(characterBufferRange.CharacterBufferReference, count, defaultProperties.WithTypeface(currentTypeface),
biDiLevel);
}
if (previousTypeface is not null)
{
if (TryGetShapeableLength(text, previousTypeface.Value, defaultTypeface, out count, out _))
if (TryGetShapeableLength(characterBufferRange, previousTypeface.Value, defaultTypeface, out count, out _))
{
return new ShapeableTextCharacters(text.Take(count),
return new ShapeableTextCharacters(characterBufferRange.CharacterBufferReference, count,
defaultProperties.WithTypeface(previousTypeface.Value), biDiLevel);
}
}
var codepoint = Codepoint.ReplacementCodepoint;
var codepointEnumerator = new CodepointEnumerator(text.Skip(count));
var codepointEnumerator = new CodepointEnumerator(characterBufferRange.Skip(count));
while (codepointEnumerator.MoveNext())
{
@ -118,10 +173,10 @@ namespace Avalonia.Media.TextFormatting
defaultTypeface.Stretch, defaultTypeface.FontFamily, defaultProperties.CultureInfo,
out currentTypeface);
if (matchFound && TryGetShapeableLength(text, currentTypeface, defaultTypeface, out count, out _))
if (matchFound && TryGetShapeableLength(characterBufferRange, currentTypeface, defaultTypeface, out count, out _))
{
//Fallback found
return new ShapeableTextCharacters(text.Take(count), defaultProperties.WithTypeface(currentTypeface),
return new ShapeableTextCharacters(characterBufferRange.CharacterBufferReference, count, defaultProperties.WithTypeface(currentTypeface),
biDiLevel);
}
@ -130,7 +185,7 @@ namespace Avalonia.Media.TextFormatting
var glyphTypeface = currentTypeface.GlyphTypeface;
var enumerator = new GraphemeEnumerator(text);
var enumerator = new GraphemeEnumerator(characterBufferRange);
while (enumerator.MoveNext())
{
@ -144,20 +199,20 @@ namespace Avalonia.Media.TextFormatting
count += grapheme.Text.Length;
}
return new ShapeableTextCharacters(text.Take(count), defaultProperties, biDiLevel);
return new ShapeableTextCharacters(characterBufferRange.CharacterBufferReference, count, defaultProperties, biDiLevel);
}
/// <summary>
/// Tries to get a shapeable length that is supported by the specified typeface.
/// </summary>
/// <param name="text">The text.</param>
/// <param name="characterBufferRange">The character buffer range to shape.</param>
/// <param name="typeface">The typeface that is used to find matching characters.</param>
/// <param name="defaultTypeface"></param>
/// <param name="length">The shapeable length.</param>
/// <param name="script"></param>
/// <returns></returns>
protected static bool TryGetShapeableLength(
ReadOnlySlice<char> text,
internal static bool TryGetShapeableLength(
CharacterBufferRange characterBufferRange,
Typeface typeface,
Typeface? defaultTypeface,
out int length,
@ -166,7 +221,7 @@ namespace Avalonia.Media.TextFormatting
length = 0;
script = Script.Unknown;
if (text.Length == 0)
if (characterBufferRange.Length == 0)
{
return false;
}
@ -174,7 +229,7 @@ namespace Avalonia.Media.TextFormatting
var font = typeface.GlyphTypeface;
var defaultFont = defaultTypeface?.GlyphTypeface;
var enumerator = new GraphemeEnumerator(text);
var enumerator = new GraphemeEnumerator(characterBufferRange);
while (enumerator.MoveNext())
{

102
src/Avalonia.Base/Media/TextFormatting/TextEllipsisHelper.cs

@ -32,86 +32,88 @@ namespace Avalonia.Media.TextFormatting
switch (currentRun)
{
case ShapedTextCharacters shapedRun:
{
currentWidth += shapedRun.Size.Width;
if (currentWidth > availableWidth)
{
if (shapedRun.TryMeasureCharacters(availableWidth, out var measuredLength))
currentWidth += shapedRun.Size.Width;
if (currentWidth > availableWidth)
{
if (isWordEllipsis && measuredLength < textLine.Length)
if (shapedRun.TryMeasureCharacters(availableWidth, out var measuredLength))
{
var currentBreakPosition = 0;
if (isWordEllipsis && measuredLength < textLine.Length)
{
var currentBreakPosition = 0;
var lineBreaker = new LineBreakEnumerator(currentRun.Text);
var text = new CharacterBufferRange(currentRun.CharacterBufferReference, currentRun.Length);
while (currentBreakPosition < measuredLength && lineBreaker.MoveNext())
{
var nextBreakPosition = lineBreaker.Current.PositionMeasure;
var lineBreaker = new LineBreakEnumerator(text);
if (nextBreakPosition == 0)
while (currentBreakPosition < measuredLength && lineBreaker.MoveNext())
{
break;
}
var nextBreakPosition = lineBreaker.Current.PositionMeasure;
if (nextBreakPosition >= measuredLength)
{
break;
if (nextBreakPosition == 0)
{
break;
}
if (nextBreakPosition >= measuredLength)
{
break;
}
currentBreakPosition = nextBreakPosition;
}
currentBreakPosition = nextBreakPosition;
measuredLength = currentBreakPosition;
}
measuredLength = currentBreakPosition;
}
}
collapsedLength += measuredLength;
collapsedLength += measuredLength;
var collapsedRuns = new List<DrawableTextRun>(textRuns.Count);
var collapsedRuns = new List<DrawableTextRun>(textRuns.Count);
if (collapsedLength > 0)
{
var splitResult = TextFormatterImpl.SplitDrawableRuns(textRuns, collapsedLength);
if (collapsedLength > 0)
{
var splitResult = TextFormatterImpl.SplitDrawableRuns(textRuns, collapsedLength);
collapsedRuns.AddRange(splitResult.First);
}
collapsedRuns.AddRange(splitResult.First);
}
collapsedRuns.Add(shapedSymbol);
collapsedRuns.Add(shapedSymbol);
return collapsedRuns;
}
return collapsedRuns;
}
availableWidth -= currentRun.Size.Width;
availableWidth -= currentRun.Size.Width;
break;
}
break;
}
case { } drawableRun:
{
//The whole run needs to fit into available space
if (currentWidth + drawableRun.Size.Width > availableWidth)
{
var collapsedRuns = new List<DrawableTextRun>(textRuns.Count);
if (collapsedLength > 0)
//The whole run needs to fit into available space
if (currentWidth + drawableRun.Size.Width > availableWidth)
{
var splitResult = TextFormatterImpl.SplitDrawableRuns(textRuns, collapsedLength);
var collapsedRuns = new List<DrawableTextRun>(textRuns.Count);
collapsedRuns.AddRange(splitResult.First);
}
if (collapsedLength > 0)
{
var splitResult = TextFormatterImpl.SplitDrawableRuns(textRuns, collapsedLength);
collapsedRuns.AddRange(splitResult.First);
}
collapsedRuns.Add(shapedSymbol);
collapsedRuns.Add(shapedSymbol);
return collapsedRuns;
}
return collapsedRuns;
break;
}
break;
}
}
collapsedLength += currentRun.TextSourceLength;
collapsedLength += currentRun.Length;
runIndex++;
}

4
src/Avalonia.Base/Media/TextFormatting/TextEndOfLine.cs

@ -7,9 +7,9 @@
{
public TextEndOfLine(int textSourceLength = DefaultTextSourceLength)
{
TextSourceLength = textSourceLength;
Length = textSourceLength;
}
public override int TextSourceLength { get; }
public override int Length { get; }
}
}

100
src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs

@ -79,14 +79,14 @@ namespace Avalonia.Media.TextFormatting
{
var currentRun = textRuns[i];
if (currentLength + currentRun.TextSourceLength < length)
if (currentLength + currentRun.Length < length)
{
currentLength += currentRun.TextSourceLength;
currentLength += currentRun.Length;
continue;
}
var firstCount = currentRun.TextSourceLength >= 1 ? i + 1 : i;
var firstCount = currentRun.Length >= 1 ? i + 1 : i;
var first = new List<DrawableTextRun>(firstCount);
@ -100,13 +100,13 @@ namespace Avalonia.Media.TextFormatting
var secondCount = textRuns.Count - firstCount;
if (currentLength + currentRun.TextSourceLength == length)
if (currentLength + currentRun.Length == length)
{
var second = secondCount > 0 ? new List<DrawableTextRun>(secondCount) : null;
if (second != null)
{
var offset = currentRun.TextSourceLength >= 1 ? 1 : 0;
var offset = currentRun.Length >= 1 ? 1 : 0;
for (var j = 0; j < secondCount; j++)
{
@ -163,15 +163,17 @@ namespace Avalonia.Media.TextFormatting
foreach (var textRun in textRuns)
{
if (textRun.Text.IsEmpty)
if (textRun.CharacterBufferReference.CharacterBuffer.Length == 0)
{
var text = new char[textRun.TextSourceLength];
var characterBuffer = new CharacterBufferReference(new char[textRun.Length]);
biDiData.Append(text);
biDiData.Append(new CharacterBufferRange(characterBuffer, textRun.Length));
}
else
{
biDiData.Append(textRun.Text);
var text = new CharacterBufferRange(textRun.CharacterBufferReference, textRun.Length);
biDiData.Append(text);
}
}
@ -207,10 +209,9 @@ namespace Avalonia.Media.TextFormatting
case ShapeableTextCharacters shapeableRun:
{
var groupedRuns = new List<ShapeableTextCharacters>(2) { shapeableRun };
var text = currentRun.Text;
var start = currentRun.Text.Start;
var length = currentRun.Text.Length;
var bufferOffset = currentRun.Text.BufferOffset;
var characterBufferReference = currentRun.CharacterBufferReference;
var length = currentRun.Length;
var offsetToFirstCharacter = characterBufferReference.OffsetToFirstChar;
while (index + 1 < processedRuns.Count)
{
@ -223,19 +224,14 @@ namespace Avalonia.Media.TextFormatting
{
groupedRuns.Add(nextRun);
length += nextRun.Text.Length;
if (start > nextRun.Text.Start)
{
start = nextRun.Text.Start;
}
length += nextRun.Length;
if (bufferOffset > nextRun.Text.BufferOffset)
if (offsetToFirstCharacter > nextRun.CharacterBufferReference.OffsetToFirstChar)
{
bufferOffset = nextRun.Text.BufferOffset;
offsetToFirstCharacter = nextRun.CharacterBufferReference.OffsetToFirstChar;
}
text = new ReadOnlySlice<char>(text.Buffer, start, length, bufferOffset);
characterBufferReference = new CharacterBufferReference(characterBufferReference.CharacterBuffer, offsetToFirstCharacter);
index++;
@ -252,7 +248,7 @@ namespace Avalonia.Media.TextFormatting
shapeableRun.BidiLevel, currentRun.Properties.CultureInfo,
paragraphProperties.DefaultIncrementalTab, paragraphProperties.LetterSpacing);
drawableTextRuns.AddRange(ShapeTogether(groupedRuns, text, shaperOptions));
drawableTextRuns.AddRange(ShapeTogether(groupedRuns, characterBufferReference, length, shaperOptions));
break;
}
@ -263,17 +259,17 @@ namespace Avalonia.Media.TextFormatting
}
private static IReadOnlyList<ShapedTextCharacters> ShapeTogether(
IReadOnlyList<ShapeableTextCharacters> textRuns, ReadOnlySlice<char> text, TextShaperOptions options)
IReadOnlyList<ShapeableTextCharacters> textRuns, CharacterBufferReference text, int length, TextShaperOptions options)
{
var shapedRuns = new List<ShapedTextCharacters>(textRuns.Count);
var shapedBuffer = TextShaper.Current.ShapeText(text, options);
var shapedBuffer = TextShaper.Current.ShapeText(text, length, options);
for (var i = 0; i < textRuns.Count; i++)
{
var currentRun = textRuns[i];
var splitResult = shapedBuffer.Split(currentRun.Text.Length);
var splitResult = shapedBuffer.Split(currentRun.Length);
shapedRuns.Add(new ShapedTextCharacters(splitResult.First, currentRun.Properties));
@ -301,7 +297,7 @@ namespace Avalonia.Media.TextFormatting
TextRunProperties? previousProperties = null;
TextCharacters? currentRun = null;
var runText = ReadOnlySlice<char>.Empty;
CharacterBufferRange runText = default;
for (var i = 0; i < textCharacters.Count; i++)
{
@ -314,12 +310,12 @@ namespace Avalonia.Media.TextFormatting
yield return new[] { drawableRun };
levelIndex += drawableRun.TextSourceLength;
levelIndex += drawableRun.Length;
continue;
}
runText = currentRun.Text;
runText = new CharacterBufferRange(currentRun.CharacterBufferReference, currentRun.Length);
for (; j < runText.Length;)
{
@ -401,7 +397,7 @@ namespace Avalonia.Media.TextFormatting
{
endOfLine = textEndOfLine;
textSourceLength += textEndOfLine.TextSourceLength;
textSourceLength += textEndOfLine.Length;
textRuns.Add(textRun);
@ -414,7 +410,7 @@ namespace Avalonia.Media.TextFormatting
{
if (TryGetLineBreak(textCharacters, out var runLineBreak))
{
var splitResult = new TextCharacters(textCharacters.Text.Take(runLineBreak.PositionWrap),
var splitResult = new TextCharacters(textCharacters.CharacterBufferReference, runLineBreak.PositionWrap,
textCharacters.Properties);
textRuns.Add(splitResult);
@ -435,7 +431,7 @@ namespace Avalonia.Media.TextFormatting
}
}
textSourceLength += textRun.TextSourceLength;
textSourceLength += textRun.Length;
}
return textRuns;
@ -445,12 +441,14 @@ namespace Avalonia.Media.TextFormatting
{
lineBreak = default;
if (textRun.Text.IsEmpty)
if (textRun.CharacterBufferReference.CharacterBuffer.IsEmpty)
{
return false;
}
var lineBreakEnumerator = new LineBreakEnumerator(textRun.Text);
var characterBufferRange = new CharacterBufferRange(textRun.CharacterBufferReference, textRun.Length);
var lineBreakEnumerator = new LineBreakEnumerator(characterBufferRange);
while (lineBreakEnumerator.MoveNext())
{
@ -461,7 +459,7 @@ namespace Avalonia.Media.TextFormatting
lineBreak = lineBreakEnumerator.Current;
return lineBreak.PositionWrap >= textRun.Text.Length || true;
return lineBreak.PositionWrap >= textRun.Length || true;
}
return false;
@ -480,7 +478,7 @@ namespace Avalonia.Media.TextFormatting
{
if(shapedTextCharacters.ShapedBuffer.Length > 0)
{
var firstCluster = shapedTextCharacters.ShapedBuffer.GlyphClusters[0];
var firstCluster = shapedTextCharacters.ShapedBuffer.GlyphInfos[0].GlyphCluster;
var lastCluster = firstCluster;
for (var i = 0; i < shapedTextCharacters.ShapedBuffer.Length; i++)
@ -498,7 +496,7 @@ namespace Avalonia.Media.TextFormatting
currentWidth += glyphInfo.GlyphAdvance;
}
measuredLength += currentRun.TextSourceLength;
measuredLength += currentRun.Length;
}
break;
@ -511,7 +509,7 @@ namespace Avalonia.Media.TextFormatting
goto found;
}
measuredLength += currentRun.TextSourceLength;
measuredLength += currentRun.Length;
currentWidth += currentRun.Size.Width;
break;
@ -533,11 +531,11 @@ namespace Avalonia.Media.TextFormatting
var flowDirection = paragraphProperties.FlowDirection;
var properties = paragraphProperties.DefaultTextRunProperties;
var glyphTypeface = properties.Typeface.GlyphTypeface;
var text = new ReadOnlySlice<char>(s_empty, firstTextSourceIndex, 1);
var glyph = glyphTypeface.GetGlyph(s_empty[0]);
var glyphInfos = new[] { new GlyphInfo(glyph, firstTextSourceIndex) };
var shapedBuffer = new ShapedBuffer(text, glyphInfos, glyphTypeface, properties.FontRenderingEmSize,
var characterBufferRange = new CharacterBufferRange(new CharacterBufferReference(s_empty), s_empty.Length);
var shapedBuffer = new ShapedBuffer(characterBufferRange, glyphInfos, glyphTypeface, properties.FontRenderingEmSize,
(sbyte)flowDirection);
var textRuns = new List<DrawableTextRun> { new ShapedTextCharacters(shapedBuffer, properties) };
@ -579,7 +577,9 @@ namespace Avalonia.Media.TextFormatting
{
var currentRun = textRuns[index];
var lineBreaker = new LineBreakEnumerator(currentRun.Text);
var runText = new CharacterBufferRange(currentRun.CharacterBufferReference, currentRun.Length);
var lineBreaker = new LineBreakEnumerator(runText);
var breakFound = false;
@ -612,7 +612,7 @@ namespace Avalonia.Media.TextFormatting
//Find next possible wrap position (overflow)
if (index < textRuns.Count - 1)
{
if (lineBreaker.Current.PositionWrap != currentRun.Text.Length)
if (lineBreaker.Current.PositionWrap != currentRun.Length)
{
//We already found the next possible wrap position.
breakFound = true;
@ -626,7 +626,7 @@ namespace Avalonia.Media.TextFormatting
{
currentPosition += lineBreaker.Current.PositionWrap;
if (lineBreaker.Current.PositionWrap != currentRun.Text.Length)
if (lineBreaker.Current.PositionWrap != currentRun.Length)
{
break;
}
@ -640,7 +640,9 @@ namespace Avalonia.Media.TextFormatting
currentRun = textRuns[index];
lineBreaker = new LineBreakEnumerator(currentRun.Text);
runText = new CharacterBufferRange(currentRun.CharacterBufferReference, currentRun.Length);
lineBreaker = new LineBreakEnumerator(runText);
}
}
else
@ -669,7 +671,7 @@ namespace Avalonia.Media.TextFormatting
if (!breakFound)
{
currentLength += currentRun.TextSourceLength;
currentLength += currentRun.Length;
continue;
}
@ -723,12 +725,12 @@ namespace Avalonia.Media.TextFormatting
return false;
}
if (Current.TextSourceLength == 0)
if (Current.Length == 0)
{
return false;
}
_pos += Current.TextSourceLength;
_pos += Current.Length;
return true;
}
@ -754,7 +756,9 @@ namespace Avalonia.Media.TextFormatting
var shaperOptions = new TextShaperOptions(glyphTypeface, fontRenderingEmSize, (sbyte)flowDirection, cultureInfo);
var shapedBuffer = textShaper.ShapeText(textRun.Text, shaperOptions);
var characterBuffer = textRun.CharacterBufferReference;
var shapedBuffer = textShaper.ShapeText(characterBuffer, textRun.Length, shaperOptions);
return new ShapedTextCharacters(shapedBuffer, textRun.Properties);
}

2
src/Avalonia.Base/Media/TextFormatting/TextLayout.cs

@ -55,7 +55,7 @@ namespace Avalonia.Media.TextFormatting
CreateTextParagraphProperties(typeface, fontSize, foreground, textAlignment, textWrapping,
textDecorations, flowDirection, lineHeight, letterSpacing);
_textSource = new FormattedTextSource(text.AsMemory(), _paragraphProperties.DefaultTextRunProperties, textStyleOverrides);
_textSource = new FormattedTextSource(text ?? "", _paragraphProperties.DefaultTextRunProperties, textStyleOverrides);
_textTrimming = textTrimming ?? TextTrimming.None;

Some files were not shown because too many files changed in this diff

Loading…
Cancel
Save