Browse Source

Merge branch 'master' into fixes/avalonia-controls-nullability

pull/7477/head
Steven Kirk 5 years ago
parent
commit
3e8cb0696c
  1. 41
      Avalonia.sln
  2. 1
      build/CoreLibraries.props
  3. 37
      samples/ControlCatalog/Pages/ScreenPage.cs
  4. 22
      samples/ControlCatalog/Pages/ViewboxPage.xaml
  5. 19
      samples/ControlCatalog/Pages/ViewboxPage.xaml.cs
  6. 3
      samples/RenderDemo/MainWindow.xaml
  7. 54
      samples/RenderDemo/Pages/ClippingPage.xaml
  8. 17
      samples/RenderDemo/Pages/ClippingPage.xaml.cs
  9. 13
      samples/RenderDemo/Pages/CustomSkiaPage.cs
  10. 7
      samples/RenderDemo/Pages/FormattedTextPage.axaml
  11. 60
      samples/RenderDemo/Pages/FormattedTextPage.axaml.cs
  12. 7
      samples/RenderDemo/Pages/GlyphRunPage.xaml.cs
  13. 5
      src/Android/Avalonia.Android/AndroidPlatform.cs
  14. 2
      src/Android/Avalonia.Android/AppBuilder.cs
  15. 4
      src/Android/Avalonia.Android/Avalonia.Android.csproj
  16. 18
      src/Android/Avalonia.Android/RuntimeInfo.cs
  17. 5
      src/Avalonia.Base/Platform/IRuntimePlatform.cs
  18. 2
      src/Avalonia.Base/Utilities/ImmutableReadOnlyListStructEnumerator.cs
  19. 6
      src/Avalonia.Controls/ApiCompatBaseline.txt
  20. 19
      src/Avalonia.Controls/Control.cs
  21. 494
      src/Avalonia.Controls/Presenters/TextPresenter.cs
  22. 6
      src/Avalonia.Controls/Primitives/PopupRoot.cs
  23. 1
      src/Avalonia.Controls/Properties/AssemblyInfo.cs
  24. 52
      src/Avalonia.Controls/TextBlock.cs
  25. 323
      src/Avalonia.Controls/TextBox.cs
  26. 28
      src/Avalonia.Controls/TextBoxTextInputMethodClient.cs
  27. 53
      src/Avalonia.Controls/Utils/StringUtils.cs
  28. 9
      src/Avalonia.Controls/ValidatingToplevel.cs
  29. 3
      src/Avalonia.DesktopRuntime/ApiCompatBaseline.txt
  30. 2
      src/Avalonia.DesktopRuntime/AppBuilder.cs
  31. 2
      src/Avalonia.DesktopRuntime/Avalonia.DesktopRuntime.csproj
  32. 40
      src/Avalonia.DesktopRuntime/RuntimeInfo.cs
  33. 36
      src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs
  34. 12
      src/Avalonia.Headless/HeadlessPlatformStubs.cs
  35. 17
      src/Avalonia.PlatformSupport/AssetLoader.cs
  36. 18
      src/Avalonia.PlatformSupport/Avalonia.PlatformSupport.csproj
  37. 65
      src/Avalonia.PlatformSupport/DynLoader.cs
  38. 79
      src/Avalonia.PlatformSupport/StandardRuntimePlatform.cs
  39. 31
      src/Avalonia.PlatformSupport/StandardRuntimePlatformServices.cs
  40. 61
      src/Avalonia.Visuals/ApiCompatBaseline.txt
  41. BIN
      src/Avalonia.Visuals/Assets/BiDi.trie
  42. BIN
      src/Avalonia.Visuals/Assets/UnicodeData.trie
  43. 10
      src/Avalonia.Visuals/Media/DrawingContext.cs
  44. 1413
      src/Avalonia.Visuals/Media/FormattedText.cs
  45. 29
      src/Avalonia.Visuals/Media/FormattedTextLine.cs
  46. 39
      src/Avalonia.Visuals/Media/FormattedTextStyleSpan.cs
  47. 290
      src/Avalonia.Visuals/Media/GlyphRun.cs
  48. 2
      src/Avalonia.Visuals/Media/GlyphTypeface.cs
  49. 25
      src/Avalonia.Visuals/Media/TextDecoration.cs
  50. 11
      src/Avalonia.Visuals/Media/TextFormatting/FontMetrics.cs
  51. 24
      src/Avalonia.Visuals/Media/TextFormatting/FormattedTextSource.cs
  52. 5
      src/Avalonia.Visuals/Media/TextFormatting/ShapeableTextCharacters.cs
  53. 293
      src/Avalonia.Visuals/Media/TextFormatting/ShapedBuffer.cs
  54. 207
      src/Avalonia.Visuals/Media/TextFormatting/ShapedTextCharacters.cs
  55. 28
      src/Avalonia.Visuals/Media/TextFormatting/SplitResult.cs
  56. 134
      src/Avalonia.Visuals/Media/TextFormatting/TextCharacters.cs
  57. 9
      src/Avalonia.Visuals/Media/TextFormatting/TextEndOfParagraph.cs
  58. 669
      src/Avalonia.Visuals/Media/TextFormatting/TextFormatterImpl.cs
  59. 567
      src/Avalonia.Visuals/Media/TextFormatting/TextLayout.cs
  60. 28
      src/Avalonia.Visuals/Media/TextFormatting/TextLine.cs
  61. 17
      src/Avalonia.Visuals/Media/TextFormatting/TextLineBreak.cs
  62. 628
      src/Avalonia.Visuals/Media/TextFormatting/TextLineImpl.cs
  63. 2
      src/Avalonia.Visuals/Media/TextFormatting/TextParagraphProperties.cs
  64. 2
      src/Avalonia.Visuals/Media/TextFormatting/TextRun.cs
  65. 6
      src/Avalonia.Visuals/Media/TextFormatting/TextShaper.cs
  66. 1717
      src/Avalonia.Visuals/Media/TextFormatting/Unicode/BiDiAlgorithm.cs
  67. 2
      src/Avalonia.Visuals/Media/TextFormatting/Unicode/BiDiClass.cs
  68. 182
      src/Avalonia.Visuals/Media/TextFormatting/Unicode/BiDiData.cs
  69. 9
      src/Avalonia.Visuals/Media/TextFormatting/Unicode/BiDiPairedBracketType.cs
  70. 58
      src/Avalonia.Visuals/Media/TextFormatting/Unicode/Codepoint.cs
  71. 331
      src/Avalonia.Visuals/Media/TextFormatting/Unicode/PropertyValueAliasHelper.cs
  72. 71
      src/Avalonia.Visuals/Media/TextFormatting/Unicode/UnicodeData.cs
  73. 27
      src/Avalonia.Visuals/Media/TextHitTestResult.cs
  74. 2
      src/Avalonia.Visuals/Media/Typeface.cs
  75. 7
      src/Avalonia.Visuals/Platform/IDrawingContextImpl.cs
  76. 59
      src/Avalonia.Visuals/Platform/IFormattedTextImpl.cs
  77. 21
      src/Avalonia.Visuals/Platform/IPlatformRenderInterface.cs
  78. 6
      src/Avalonia.Visuals/Platform/ITextShaperImpl.cs
  79. 5
      src/Avalonia.Visuals/Rendering/DeferredRenderer.cs
  80. 2
      src/Avalonia.Visuals/Rendering/ImmediateRenderer.cs
  81. 30
      src/Avalonia.Visuals/Rendering/RendererBase.cs
  82. 15
      src/Avalonia.Visuals/Rendering/SceneGraph/DeferredDrawingContextImpl.cs
  83. 89
      src/Avalonia.Visuals/Rendering/SceneGraph/TextNode.cs
  84. 184
      src/Avalonia.Visuals/Utilities/ArrayBuilder.cs
  85. 197
      src/Avalonia.Visuals/Utilities/ArraySlice.cs
  86. 80
      src/Avalonia.Visuals/Utilities/BinarySearchExtension.cs
  87. 2360
      src/Avalonia.Visuals/Utilities/FrugalList.cs
  88. 58
      src/Avalonia.Visuals/Utilities/MappedArraySlice.cs
  89. 80
      src/Avalonia.Visuals/Utilities/ReadOnlySlice.cs
  90. 596
      src/Avalonia.Visuals/Utilities/Span.cs
  91. 17
      src/Shared/PlatformSupport/PlatformSupport.projitems
  92. 11
      src/Shared/PlatformSupport/PlatformSupport.shproj
  93. 28
      src/Shared/PlatformSupport/StandardRuntimePlatformServices.cs
  94. 13
      src/Shared/RenderHelpers/QuadBezierHelper.cs
  95. 14
      src/Shared/RenderHelpers/RenderHelpers.projitems
  96. 13
      src/Shared/RenderHelpers/RenderHelpers.shproj
  97. 84
      src/Shared/WindowResizeDragHelper.cs
  98. 3
      src/Skia/Avalonia.Skia/Avalonia.Skia.csproj
  99. 12
      src/Skia/Avalonia.Skia/DrawingContextImpl.cs
  100. 838
      src/Skia/Avalonia.Skia/FormattedTextImpl.cs

41
Avalonia.sln

@ -60,20 +60,17 @@ EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Shared", "Shared", "{A689DEF5-D50F-4975-8B72-124C9EB54066}" Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Shared", "Shared", "{A689DEF5-D50F-4975-8B72-124C9EB54066}"
ProjectSection(SolutionItems) = preProject ProjectSection(SolutionItems) = preProject
.editorconfig = .editorconfig .editorconfig = .editorconfig
src\Shared\ModuleInitializer.cs = src\Shared\ModuleInitializer.cs
EndProjectSection EndProjectSection
EndProject EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.ReactiveUI", "src\Avalonia.ReactiveUI\Avalonia.ReactiveUI.csproj", "{6417B24E-49C2-4985-8DB2-3AB9D898EC91}" Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.ReactiveUI", "src\Avalonia.ReactiveUI\Avalonia.ReactiveUI.csproj", "{6417B24E-49C2-4985-8DB2-3AB9D898EC91}"
EndProject EndProject
Project("{D954291E-2A0B-460D-934E-DC6B0785DB48}") = "PlatformSupport", "src\Shared\PlatformSupport\PlatformSupport.shproj", "{E4D9629C-F168-4224-3F51-A5E482FFBC42}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Markup", "src\Markup\Avalonia.Markup\Avalonia.Markup.csproj", "{6417E941-21BC-467B-A771-0DE389353CE6}" Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Markup", "src\Markup\Avalonia.Markup\Avalonia.Markup.csproj", "{6417E941-21BC-467B-A771-0DE389353CE6}"
EndProject EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Markup.UnitTests", "tests\Avalonia.Markup.UnitTests\Avalonia.Markup.UnitTests.csproj", "{8EF392D5-1416-45AA-9956-7CBBC3229E8A}" Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Markup.UnitTests", "tests\Avalonia.Markup.UnitTests\Avalonia.Markup.UnitTests.csproj", "{8EF392D5-1416-45AA-9956-7CBBC3229E8A}"
EndProject EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BindingDemo", "samples\BindingDemo\BindingDemo.csproj", "{08B3E6B9-1CD5-443C-9F61-6D49D1C5F162}" Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BindingDemo", "samples\BindingDemo\BindingDemo.csproj", "{08B3E6B9-1CD5-443C-9F61-6D49D1C5F162}"
EndProject EndProject
Project("{D954291E-2A0B-460D-934E-DC6B0785DB48}") = "RenderHelpers", "src\Shared\RenderHelpers\RenderHelpers.shproj", "{3C4C0CB4-0C0F-4450-A37B-148C84FF905F}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Skia", "Skia", "{3743B0F2-CC41-4F14-A8C8-267F579BF91E}" Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Skia", "Skia", "{3743B0F2-CC41-4F14-A8C8-267F579BF91E}"
EndProject EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Android", "Android", "{7CF9789C-F1D3-4D0E-90E5-F1DF67A2753F}" Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Android", "Android", "{7CF9789C-F1D3-4D0E-90E5-F1DF67A2753F}"
@ -235,15 +232,9 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WindowsInteropTest", "sampl
EndProject EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ControlSamples", "samples\SampleControls\ControlSamples.csproj", "{A0D0A6A4-5C72-4ADA-9B27-621C7D94F270}" Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ControlSamples", "samples\SampleControls\ControlSamples.csproj", "{A0D0A6A4-5C72-4ADA-9B27-621C7D94F270}"
EndProject EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.PlatformSupport", "src\Avalonia.PlatformSupport\Avalonia.PlatformSupport.csproj", "{E8A597F0-2AB5-4BDA-A235-41162DAF53CF}"
EndProject
Global Global
GlobalSection(SharedMSBuildProjectFiles) = preSolution
src\Shared\RenderHelpers\RenderHelpers.projitems*{3c4c0cb4-0c0f-4450-a37b-148c84ff905f}*SharedItemsImports = 13
src\Shared\RenderHelpers\RenderHelpers.projitems*{3e908f67-5543-4879-a1dc-08eace79b3cd}*SharedItemsImports = 5
src\Shared\PlatformSupport\PlatformSupport.projitems*{7b92af71-6287-4693-9dcb-bd5b6e927e23}*SharedItemsImports = 5
src\Shared\RenderHelpers\RenderHelpers.projitems*{7d2d3083-71dd-4cc9-8907-39a0d86fb322}*SharedItemsImports = 5
src\Shared\PlatformSupport\PlatformSupport.projitems*{88060192-33d5-4932-b0f9-8bd2763e857d}*SharedItemsImports = 5
src\Shared\PlatformSupport\PlatformSupport.projitems*{e4d9629c-f168-4224-3f51-a5e482ffbc42}*SharedItemsImports = 13
EndGlobalSection
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution
Ad-Hoc|Any CPU = Ad-Hoc|Any CPU Ad-Hoc|Any CPU = Ad-Hoc|Any CPU
Ad-Hoc|iPhone = Ad-Hoc|iPhone Ad-Hoc|iPhone = Ad-Hoc|iPhone
@ -2169,6 +2160,30 @@ Global
{A0D0A6A4-5C72-4ADA-9B27-621C7D94F270}.Release|iPhone.Build.0 = Release|Any CPU {A0D0A6A4-5C72-4ADA-9B27-621C7D94F270}.Release|iPhone.Build.0 = Release|Any CPU
{A0D0A6A4-5C72-4ADA-9B27-621C7D94F270}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU {A0D0A6A4-5C72-4ADA-9B27-621C7D94F270}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU
{A0D0A6A4-5C72-4ADA-9B27-621C7D94F270}.Release|iPhoneSimulator.Build.0 = Release|Any CPU {A0D0A6A4-5C72-4ADA-9B27-621C7D94F270}.Release|iPhoneSimulator.Build.0 = Release|Any CPU
{E8A597F0-2AB5-4BDA-A235-41162DAF53CF}.Ad-Hoc|Any CPU.ActiveCfg = Debug|Any CPU
{E8A597F0-2AB5-4BDA-A235-41162DAF53CF}.Ad-Hoc|Any CPU.Build.0 = Debug|Any CPU
{E8A597F0-2AB5-4BDA-A235-41162DAF53CF}.Ad-Hoc|iPhone.ActiveCfg = Debug|Any CPU
{E8A597F0-2AB5-4BDA-A235-41162DAF53CF}.Ad-Hoc|iPhone.Build.0 = Debug|Any CPU
{E8A597F0-2AB5-4BDA-A235-41162DAF53CF}.Ad-Hoc|iPhoneSimulator.ActiveCfg = Debug|Any CPU
{E8A597F0-2AB5-4BDA-A235-41162DAF53CF}.Ad-Hoc|iPhoneSimulator.Build.0 = Debug|Any CPU
{E8A597F0-2AB5-4BDA-A235-41162DAF53CF}.AppStore|Any CPU.ActiveCfg = Debug|Any CPU
{E8A597F0-2AB5-4BDA-A235-41162DAF53CF}.AppStore|Any CPU.Build.0 = Debug|Any CPU
{E8A597F0-2AB5-4BDA-A235-41162DAF53CF}.AppStore|iPhone.ActiveCfg = Debug|Any CPU
{E8A597F0-2AB5-4BDA-A235-41162DAF53CF}.AppStore|iPhone.Build.0 = Debug|Any CPU
{E8A597F0-2AB5-4BDA-A235-41162DAF53CF}.AppStore|iPhoneSimulator.ActiveCfg = Debug|Any CPU
{E8A597F0-2AB5-4BDA-A235-41162DAF53CF}.AppStore|iPhoneSimulator.Build.0 = Debug|Any CPU
{E8A597F0-2AB5-4BDA-A235-41162DAF53CF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{E8A597F0-2AB5-4BDA-A235-41162DAF53CF}.Debug|Any CPU.Build.0 = Debug|Any CPU
{E8A597F0-2AB5-4BDA-A235-41162DAF53CF}.Debug|iPhone.ActiveCfg = Debug|Any CPU
{E8A597F0-2AB5-4BDA-A235-41162DAF53CF}.Debug|iPhone.Build.0 = Debug|Any CPU
{E8A597F0-2AB5-4BDA-A235-41162DAF53CF}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU
{E8A597F0-2AB5-4BDA-A235-41162DAF53CF}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU
{E8A597F0-2AB5-4BDA-A235-41162DAF53CF}.Release|Any CPU.ActiveCfg = Release|Any CPU
{E8A597F0-2AB5-4BDA-A235-41162DAF53CF}.Release|Any CPU.Build.0 = Release|Any CPU
{E8A597F0-2AB5-4BDA-A235-41162DAF53CF}.Release|iPhone.ActiveCfg = Release|Any CPU
{E8A597F0-2AB5-4BDA-A235-41162DAF53CF}.Release|iPhone.Build.0 = Release|Any CPU
{E8A597F0-2AB5-4BDA-A235-41162DAF53CF}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU
{E8A597F0-2AB5-4BDA-A235-41162DAF53CF}.Release|iPhoneSimulator.Build.0 = Release|Any CPU
EndGlobalSection EndGlobalSection
GlobalSection(SolutionProperties) = preSolution GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE HideSolutionNode = FALSE
@ -2187,11 +2202,9 @@ Global
{EFB11458-9CDF-41C0-BE4F-44AF45A4CAB8} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B} {EFB11458-9CDF-41C0-BE4F-44AF45A4CAB8} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B}
{99135EAB-653D-47E4-A378-C96E1278CA44} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B} {99135EAB-653D-47E4-A378-C96E1278CA44} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B}
{3E53A01A-B331-47F3-B828-4A5717E77A24} = {8B6A8209-894F-4BA1-B880-965FD453982C} {3E53A01A-B331-47F3-B828-4A5717E77A24} = {8B6A8209-894F-4BA1-B880-965FD453982C}
{E4D9629C-F168-4224-3F51-A5E482FFBC42} = {A689DEF5-D50F-4975-8B72-124C9EB54066}
{6417E941-21BC-467B-A771-0DE389353CE6} = {8B6A8209-894F-4BA1-B880-965FD453982C} {6417E941-21BC-467B-A771-0DE389353CE6} = {8B6A8209-894F-4BA1-B880-965FD453982C}
{8EF392D5-1416-45AA-9956-7CBBC3229E8A} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B} {8EF392D5-1416-45AA-9956-7CBBC3229E8A} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B}
{08B3E6B9-1CD5-443C-9F61-6D49D1C5F162} = {9B9E3891-2366-4253-A952-D08BCEB71098} {08B3E6B9-1CD5-443C-9F61-6D49D1C5F162} = {9B9E3891-2366-4253-A952-D08BCEB71098}
{3C4C0CB4-0C0F-4450-A37B-148C84FF905F} = {A689DEF5-D50F-4975-8B72-124C9EB54066}
{7B92AF71-6287-4693-9DCB-BD5B6E927E23} = {7CF9789C-F1D3-4D0E-90E5-F1DF67A2753F} {7B92AF71-6287-4693-9DCB-BD5B6E927E23} = {7CF9789C-F1D3-4D0E-90E5-F1DF67A2753F}
{FF69B927-C545-49AE-8E16-3D14D621AA12} = {7CF9789C-F1D3-4D0E-90E5-F1DF67A2753F} {FF69B927-C545-49AE-8E16-3D14D621AA12} = {7CF9789C-F1D3-4D0E-90E5-F1DF67A2753F}
{4488AD85-1495-4809-9AA4-DDFE0A48527E} = {0CB0B92E-6CFF-4240-80A5-CCAFE75D91E1} {4488AD85-1495-4809-9AA4-DDFE0A48527E} = {0CB0B92E-6CFF-4240-80A5-CCAFE75D91E1}

1
build/CoreLibraries.props

@ -17,5 +17,6 @@
<ProjectReference Include="$(MSBuildThisFileDirectory)/../src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj" /> <ProjectReference Include="$(MSBuildThisFileDirectory)/../src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj" />
<ProjectReference Include="$(MSBuildThisFileDirectory)/../src/Avalonia.MicroCom/Avalonia.MicroCom.csproj" /> <ProjectReference Include="$(MSBuildThisFileDirectory)/../src/Avalonia.MicroCom/Avalonia.MicroCom.csproj" />
<ProjectReference Include="$(MSBuildThisFileDirectory)/../src/Avalonia.DesktopRuntime/Avalonia.DesktopRuntime.csproj" Condition="'$(TargetFramework)' != 'netstandard2.0'" /> <ProjectReference Include="$(MSBuildThisFileDirectory)/../src/Avalonia.DesktopRuntime/Avalonia.DesktopRuntime.csproj" Condition="'$(TargetFramework)' != 'netstandard2.0'" />
<ProjectReference Include="$(MSBuildThisFileDirectory)/../src/Avalonia.PlatformSupport/Avalonia.PlatformSupport.csproj" />
</ItemGroup> </ItemGroup>
</Project> </Project>

37
samples/ControlCatalog/Pages/ScreenPage.cs

@ -1,4 +1,5 @@
using System; using System;
using System.Globalization;
using Avalonia; using Avalonia;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Markup.Xaml; using Avalonia.Markup.Xaml;
@ -49,25 +50,33 @@ namespace ControlCatalog.Pages
context.DrawRectangle(p, boundsRect); context.DrawRectangle(p, boundsRect);
context.DrawRectangle(p, workingAreaRect); context.DrawRectangle(p, workingAreaRect);
var text = new FormattedText() { Typeface = new Typeface("Arial"), FontSize = 18 };
text.Text = $"Bounds: {screen.Bounds.TopLeft} {screen.Bounds.Width}:{screen.Bounds.Height}"; var formattedText = CreateFormattedText($"Bounds: {screen.Bounds.Width}:{screen.Bounds.Height}");
context.DrawText(drawBrush, boundsRect.Position.WithY(boundsRect.Size.Height), text); context.DrawText(formattedText, boundsRect.Position.WithY(boundsRect.Size.Height));
text.Text = $"WorkArea: {screen.WorkingArea.TopLeft} {screen.WorkingArea.Width}:{screen.WorkingArea.Height}";
context.DrawText(drawBrush, boundsRect.Position.WithY(boundsRect.Size.Height + 20), text);
text.Text = $"Scaling: {screen.PixelDensity * 100}%"; formattedText =
context.DrawText(drawBrush, boundsRect.Position.WithY(boundsRect.Size.Height + 40), text); CreateFormattedText($"WorkArea: {screen.WorkingArea.Width}:{screen.WorkingArea.Height}");
context.DrawText(formattedText, boundsRect.Position.WithY(boundsRect.Size.Height + 20));
text.Text = $"Primary: {screen.Primary}";
context.DrawText(drawBrush, boundsRect.Position.WithY(boundsRect.Size.Height + 60), text); formattedText = CreateFormattedText($"Scaling: {screen.PixelDensity * 100}%");
context.DrawText(formattedText, boundsRect.Position.WithY(boundsRect.Size.Height + 40));
text.Text = $"Current: {screen.Equals(w.Screens.ScreenFromBounds(new PixelRect(w.Position, PixelSize.FromSize(w.Bounds.Size, scaling))))}";
context.DrawText(drawBrush, boundsRect.Position.WithY(boundsRect.Size.Height + 80), text); formattedText = CreateFormattedText($"Primary: {screen.Primary}");
context.DrawText(formattedText, boundsRect.Position.WithY(boundsRect.Size.Height + 60));
formattedText =
CreateFormattedText(
$"Current: {screen.Equals(w.Screens.ScreenFromBounds(new PixelRect(w.Position, PixelSize.FromSize(w.Bounds.Size, scaling))))}");
context.DrawText(formattedText, boundsRect.Position.WithY(boundsRect.Size.Height + 80));
} }
context.DrawRectangle(p, new Rect(w.Position.X / 10f + Math.Abs(_leftMost), w.Position.Y / 10f, w.Bounds.Width / 10, w.Bounds.Height / 10)); context.DrawRectangle(p, new Rect(w.Position.X / 10f + Math.Abs(_leftMost), w.Position.Y / 10f, w.Bounds.Width / 10, w.Bounds.Height / 10));
} }
private FormattedText CreateFormattedText(string textToFormat)
{
return new FormattedText(textToFormat, CultureInfo.CurrentCulture, FlowDirection.LeftToRight,
Typeface.Default, 12, Brushes.Green);
}
} }
} }

22
samples/ControlCatalog/Pages/ViewboxPage.xaml

@ -1,5 +1,6 @@
<UserControl xmlns="https://github.com/avaloniaui" <UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:collections="clr-namespace:System.Collections;assembly=netstandard"
x:Class="ControlCatalog.Pages.ViewboxPage"> x:Class="ControlCatalog.Pages.ViewboxPage">
<Grid RowDefinitions="Auto,*,*"> <Grid RowDefinitions="Auto,*,*">
@ -12,8 +13,8 @@
<Border HorizontalAlignment="Center" Grid.Column="0" BorderThickness="1" BorderBrush="Orange" Width="200" Height="200"> <Border HorizontalAlignment="Center" Grid.Column="0" BorderThickness="1" BorderBrush="Orange" Width="200" Height="200">
<Border VerticalAlignment="Center" HorizontalAlignment="Center" Grid.Column="0" BorderThickness="1" BorderBrush="CornflowerBlue" Width="{Binding #WidthSlider.Value}" Height="{Binding #HeightSlider.Value}" > <Border VerticalAlignment="Center" HorizontalAlignment="Center" Grid.Column="0" BorderThickness="1" BorderBrush="CornflowerBlue" Width="{Binding #WidthSlider.Value}" Height="{Binding #HeightSlider.Value}" >
<Viewbox <Viewbox
Stretch="{Binding #StretchSelector.SelectedItem}" Stretch="{Binding #StretchSelector.SelectedItem, FallbackValue={x:Static Stretch.Uniform}}"
StretchDirection="{Binding #StretchDirectionSelector.SelectedItem}"> StretchDirection="{Binding #StretchDirectionSelector.SelectedItem, FallbackValue={x:Static StretchDirection.Both}}">
<Ellipse Width="50" Height="50" Fill="CornflowerBlue" /> <Ellipse Width="50" Height="50" Fill="CornflowerBlue" />
</Viewbox> </Viewbox>
</Border> </Border>
@ -25,9 +26,22 @@
<TextBlock Text="Height" /> <TextBlock Text="Height" />
<Slider Minimum="10" Maximum="200" Value="100" x:Name="HeightSlider" TickFrequency="25" TickPlacement="TopLeft" /> <Slider Minimum="10" Maximum="200" Value="100" x:Name="HeightSlider" TickFrequency="25" TickPlacement="TopLeft" />
<TextBlock Text="Stretch" /> <TextBlock Text="Stretch" />
<ComboBox x:Name="StretchSelector" HorizontalAlignment="Stretch" Margin="0,0,0,2" /> <ComboBox x:Name="StretchSelector" HorizontalAlignment="Stretch" SelectedIndex="0" Margin="0,0,0,2">
<collections:ArrayList>
<Stretch>Uniform</Stretch>
<Stretch>UniformToFill</Stretch>
<Stretch>Fill</Stretch>
<Stretch>None</Stretch>
</collections:ArrayList>
</ComboBox>
<TextBlock Text="Stretch Direction" /> <TextBlock Text="Stretch Direction" />
<ComboBox x:Name="StretchDirectionSelector" HorizontalAlignment="Stretch" /> <ComboBox x:Name="StretchDirectionSelector" SelectedIndex="0" HorizontalAlignment="Stretch">
<collections:ArrayList>
<StretchDirection>Both</StretchDirection>
<StretchDirection>DownOnly</StretchDirection>
<StretchDirection>UpOnly</StretchDirection>
</collections:ArrayList>
</ComboBox>
</StackPanel> </StackPanel>
</Grid> </Grid>

19
samples/ControlCatalog/Pages/ViewboxPage.xaml.cs

@ -1,6 +1,5 @@
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Markup.Xaml; using Avalonia.Markup.Xaml;
using Avalonia.Media;
namespace ControlCatalog.Pages namespace ControlCatalog.Pages
{ {
@ -9,24 +8,6 @@ namespace ControlCatalog.Pages
public ViewboxPage() public ViewboxPage()
{ {
InitializeComponent(); InitializeComponent();
var stretchSelector = this.FindControl<ComboBox>("StretchSelector");
stretchSelector.Items = new[]
{
Stretch.Uniform, Stretch.UniformToFill, Stretch.Fill, Stretch.None
};
stretchSelector.SelectedIndex = 0;
var stretchDirectionSelector = this.FindControl<ComboBox>("StretchDirectionSelector");
stretchDirectionSelector.Items = new[]
{
StretchDirection.Both, StretchDirection.DownOnly, StretchDirection.UpOnly
};
stretchDirectionSelector.SelectedIndex = 0;
} }
private void InitializeComponent() private void InitializeComponent()

3
samples/RenderDemo/MainWindow.xaml

@ -57,6 +57,9 @@
<TabItem Header="GlyphRun"> <TabItem Header="GlyphRun">
<pages:GlyphRunPage /> <pages:GlyphRunPage />
</TabItem> </TabItem>
<TabItem Header="FormattedText">
<pages:FormattedTextPage />
</TabItem>
<TabItem Header="LineBounds"> <TabItem Header="LineBounds">
<pages:LineBoundsPage /> <pages:LineBoundsPage />
</TabItem> </TabItem>

54
samples/RenderDemo/Pages/ClippingPage.xaml

@ -19,30 +19,36 @@
</Style> </Style>
</Styles> </Styles>
</Grid.Styles> </Grid.Styles>
<Border Name="clipped" <Border Name="clipped"
Background="Yellow" Background="Yellow"
Width="100" Width="100"
Height="100" Height="100"
Clip="M 58.625 0.07421875 Classes.clip="{Binding #useMask.IsChecked}">
C 50.305778 0.26687364 42.411858 7.0346526 41.806641 15.595703 <Border.Styles>
C 42.446442 22.063923 39.707425 13.710754 36.982422 12.683594 <Style Selector="Border.clip">
C 29.348395 6.1821635 16.419398 8.4359222 11.480469 17.195312 <Setter Property="Clip"
C 6.0935256 25.476803 9.8118851 37.71125 18.8125 41.6875 Value="M 58.625 0.07421875
C 9.1554771 40.62945 -0.070876925 49.146842 0.21679688 58.857422 C 50.305778 0.26687364 42.411858 7.0346526 41.806641 15.595703
C 0.21545578 60.872512 0.56758794 62.88911 1.2617188 64.78125 C 42.446442 22.063923 39.707425 13.710754 36.982422 12.683594
C 4.3821886 74.16708 16.298268 78.921772 25.03125 74.326172 C 29.348395 6.1821635 16.419398 8.4359222 11.480469 17.195312
C 28.266843 72.062552 26.298191 74.214838 25.414062 76.398438 C 6.0935256 25.476803 9.8118851 37.71125 18.8125 41.6875
C 21.407348 85.589198 27.295992 97.294293 37.097656 99.501953 C 9.1554771 40.62945 -0.070876925 49.146842 0.21679688 58.857422
C 46.864883 102.3541 57.82177 94.726518 58.539062 84.580078 C 0.21545578 60.872512 0.56758794 62.88911 1.2617188 64.78125
C 58.142158 79.498998 59.307538 83.392694 61.207031 85.433594 C 4.3821886 74.16708 16.298268 78.921772 25.03125 74.326172
C 67.532324 93.056874 80.440232 93.192029 86.882812 85.630859 C 28.266843 72.062552 26.298191 74.214838 25.414062 76.398438
C 93.836392 78.456939 92.396838 65.538666 84.115234 60.009766 C 21.407348 85.589198 27.295992 97.294293 37.097656 99.501953
C 79.783641 57.904836 83.569793 58.802369 86.375 58.193359 C 46.864883 102.3541 57.82177 94.726518 58.539062 84.580078
C 96.383335 56.457569 102.87506 44.824101 99.083984 35.394531 C 58.142158 79.498998 59.307538 83.392694 61.207031 85.433594
C 95.963498 26.008711 84.047451 21.254079 75.314453 25.849609 C 67.532324 93.056874 80.440232 93.192029 86.882812 85.630859
C 72.078834 28.113269 74.047517 25.960974 74.931641 23.777344 C 93.836392 78.456939 92.396838 65.538666 84.115234 60.009766
C 78.93827 14.586564 73.049722 2.8815081 63.248047 0.67382812 C 79.783641 57.904836 83.569793 58.802369 86.375 58.193359
C 61.721916 0.22817968 60.165597 0.038541919 58.625 0.07421875 z "> C 96.383335 56.457569 102.87506 44.824101 99.083984 35.394531
C 95.963498 26.008711 84.047451 21.254079 75.314453 25.849609
C 72.078834 28.113269 74.047517 25.960974 74.931641 23.777344
C 78.93827 14.586564 73.049722 2.8815081 63.248047 0.67382812
C 61.721916 0.22817968 60.165597 0.038541919 58.625 0.07421875 z " />
</Style>
</Border.Styles>
<Border Name="clipChild" Background="Red" Margin="4"> <Border Name="clipChild" Background="Red" Margin="4">
<!-- Setting opacity puts the TextBox on a new layer --> <!-- Setting opacity puts the TextBox on a new layer -->
<TextBox Text="Avalonia" Opacity="0.9" VerticalAlignment="Center"/> <TextBox Text="Avalonia" Opacity="0.9" VerticalAlignment="Center"/>

17
samples/RenderDemo/Pages/ClippingPage.xaml.cs

@ -1,35 +1,18 @@
using System;
using System.Reactive.Linq;
using Avalonia;
using Avalonia.Animation;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Data;
using Avalonia.Markup.Xaml; using Avalonia.Markup.Xaml;
using Avalonia.Media;
namespace RenderDemo.Pages namespace RenderDemo.Pages
{ {
public class ClippingPage : UserControl public class ClippingPage : UserControl
{ {
private Geometry _clip;
public ClippingPage() public ClippingPage()
{ {
InitializeComponent(); InitializeComponent();
WireUpCheckbox();
} }
private void InitializeComponent() private void InitializeComponent()
{ {
AvaloniaXamlLoader.Load(this); AvaloniaXamlLoader.Load(this);
} }
private void WireUpCheckbox()
{
var useMask = this.FindControl<CheckBox>("useMask");
var clipped = this.FindControl<Border>("clipped");
_clip = clipped.Clip;
useMask.Click += (s, e) => clipped.Clip = clipped.Clip == null ? _clip : null;
}
} }
} }

13
samples/RenderDemo/Pages/CustomSkiaPage.cs

@ -1,5 +1,6 @@
using System; using System;
using System.Diagnostics; using System.Diagnostics;
using System.Globalization;
using Avalonia; using Avalonia;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Media; using Avalonia.Media;
@ -41,7 +42,10 @@ namespace RenderDemo.Pages
{ {
var canvas = (context as ISkiaDrawingContextImpl)?.SkCanvas; var canvas = (context as ISkiaDrawingContextImpl)?.SkCanvas;
if (canvas == null) if (canvas == null)
context.DrawText(Brushes.Black, new Point(), _noSkia.PlatformImpl); using (var c = new DrawingContext(context, false))
{
c.DrawText(_noSkia, new Point());
}
else else
{ {
canvas.Save(); canvas.Save();
@ -108,10 +112,9 @@ namespace RenderDemo.Pages
public override void Render(DrawingContext context) public override void Render(DrawingContext context)
{ {
var noSkia = new FormattedText() var noSkia = new FormattedText("Current rendering API is not Skia", CultureInfo.CurrentCulture,
{ FlowDirection.LeftToRight, Typeface.Default, 12, Brushes.Black);
Text = "Current rendering API is not Skia"
};
context.Custom(new CustomDrawOp(new Rect(0, 0, Bounds.Width, Bounds.Height), noSkia)); context.Custom(new CustomDrawOp(new Rect(0, 0, Bounds.Width, Bounds.Height), noSkia));
Dispatcher.UIThread.InvokeAsync(InvalidateVisual, DispatcherPriority.Background); Dispatcher.UIThread.InvokeAsync(InvalidateVisual, DispatcherPriority.Background);
} }

7
samples/RenderDemo/Pages/FormattedTextPage.axaml

@ -0,0 +1,7 @@
<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="RenderDemo.Pages.FormattedTextPage">
</UserControl>

60
samples/RenderDemo/Pages/FormattedTextPage.axaml.cs

@ -0,0 +1,60 @@
using System.Globalization;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
using Avalonia.Media;
namespace RenderDemo.Pages
{
public class FormattedTextPage : UserControl
{
public FormattedTextPage()
{
this.InitializeComponent();
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
public override void Render(DrawingContext context)
{
const string testString = "Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor";
// Create the initial formatted text string.
var formattedText = new FormattedText(
testString,
CultureInfo.GetCultureInfo("en-us"),
FlowDirection.LeftToRight,
new Typeface("Verdana"),
32,
Brushes.Black) { MaxTextWidth = 300, MaxTextHeight = 240 };
// Set a maximum width and height. If the text overflows these values, an ellipsis "..." appears.
// Use a larger font size beginning at the first (zero-based) character and continuing for 5 characters.
// The font size is calculated in terms of points -- not as device-independent pixels.
formattedText.SetFontSize(36 * (96.0 / 72.0), 0, 5);
// Use a Bold font weight beginning at the 6th character and continuing for 11 characters.
formattedText.SetFontWeight(FontWeight.Bold, 6, 11);
var gradient = new LinearGradientBrush
{
GradientStops =
new GradientStops { new GradientStop(Colors.Orange, 0), new GradientStop(Colors.Teal, 1) },
StartPoint = new RelativePoint(0,0, RelativeUnit.Relative),
EndPoint = new RelativePoint(0,1, RelativeUnit.Relative)
};
// Use a linear gradient brush beginning at the 6th character and continuing for 11 characters.
formattedText.SetForegroundBrush(gradient, 6, 11);
// Use an Italic font style beginning at the 28th character and continuing for 28 characters.
formattedText.SetFontStyle(FontStyle.Italic, 28, 28);
context.DrawText(formattedText, new Point(10, 0));
}
}
}

7
samples/RenderDemo/Pages/GlyphRunPage.xaml.cs

@ -13,6 +13,7 @@ namespace RenderDemo.Pages
private GlyphTypeface _glyphTypeface = Typeface.Default.GlyphTypeface; private GlyphTypeface _glyphTypeface = Typeface.Default.GlyphTypeface;
private readonly Random _rand = new Random(); private readonly Random _rand = new Random();
private ushort[] _glyphIndices = new ushort[1]; private ushort[] _glyphIndices = new ushort[1];
private char[] _characters = new char[1];
private float _fontSize = 20; private float _fontSize = 20;
private int _direction = 10; private int _direction = 10;
@ -38,7 +39,7 @@ namespace RenderDemo.Pages
private void UpdateGlyphRun() private void UpdateGlyphRun()
{ {
var c = (uint)_rand.Next(65, 90); var c = (char)_rand.Next(65, 90);
if (_fontSize + _direction > 200) if (_fontSize + _direction > 200)
{ {
@ -54,6 +55,8 @@ namespace RenderDemo.Pages
_glyphIndices[0] = _glyphTypeface.GetGlyph(c); _glyphIndices[0] = _glyphTypeface.GetGlyph(c);
_characters[0] = c;
var scale = (double)_fontSize / _glyphTypeface.DesignEmHeight; var scale = (double)_fontSize / _glyphTypeface.DesignEmHeight;
var drawingGroup = new DrawingGroup(); var drawingGroup = new DrawingGroup();
@ -61,7 +64,7 @@ namespace RenderDemo.Pages
var glyphRunDrawing = new GlyphRunDrawing var glyphRunDrawing = new GlyphRunDrawing
{ {
Foreground = Brushes.Black, Foreground = Brushes.Black,
GlyphRun = new GlyphRun(_glyphTypeface, _fontSize, _glyphIndices), GlyphRun = new GlyphRun(_glyphTypeface, _fontSize, _characters, _glyphIndices)
}; };
drawingGroup.Children.Add(glyphRunDrawing); drawingGroup.Children.Add(glyphRunDrawing);

5
src/Android/Avalonia.Android/AndroidPlatform.cs

@ -10,7 +10,7 @@ using Avalonia.Input.Platform;
using Avalonia.OpenGL.Egl; using Avalonia.OpenGL.Egl;
using Avalonia.Platform; using Avalonia.Platform;
using Avalonia.Rendering; using Avalonia.Rendering;
using Avalonia.Shared.PlatformSupport; using Avalonia.PlatformSupport;
using Avalonia.Skia; using Avalonia.Skia;
namespace Avalonia namespace Avalonia
@ -59,8 +59,7 @@ namespace Avalonia.Android
.Bind<IPlatformIconLoader>().ToSingleton<PlatformIconLoaderStub>() .Bind<IPlatformIconLoader>().ToSingleton<PlatformIconLoaderStub>()
.Bind<IRenderTimer>().ToConstant(new ChoreographerTimer()) .Bind<IRenderTimer>().ToConstant(new ChoreographerTimer())
.Bind<IRenderLoop>().ToConstant(new RenderLoop()) .Bind<IRenderLoop>().ToConstant(new RenderLoop())
.Bind<PlatformHotkeyConfiguration>().ToSingleton<PlatformHotkeyConfiguration>() .Bind<PlatformHotkeyConfiguration>().ToSingleton<PlatformHotkeyConfiguration>();
.Bind<IAssetLoader>().ToConstant(new AssetLoader(appType.Assembly));
SkiaPlatform.Initialize(); SkiaPlatform.Initialize();

2
src/Android/Avalonia.Android/AppBuilder.cs

@ -1,5 +1,5 @@
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Shared.PlatformSupport; using Avalonia.PlatformSupport;
namespace Avalonia namespace Avalonia
{ {

4
src/Android/Avalonia.Android/Avalonia.Android.csproj

@ -5,9 +5,11 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\..\..\packages\Avalonia\Avalonia.csproj" /> <ProjectReference Include="..\..\..\packages\Avalonia\Avalonia.csproj" />
<ProjectReference Include="..\..\Avalonia.PlatformSupport\Avalonia.PlatformSupport.csproj">
<SetTargetFramework>TargetFramework=netstandard2.0</SetTargetFramework>
</ProjectReference>
<ProjectReference Include="..\..\Skia\Avalonia.Skia\Avalonia.Skia.csproj" /> <ProjectReference Include="..\..\Skia\Avalonia.Skia\Avalonia.Skia.csproj" />
</ItemGroup> </ItemGroup>
<Import Project="..\..\Shared\PlatformSupport\PlatformSupport.projitems" Label="Shared" />
<Import Project="..\..\..\build\Rx.props" /> <Import Project="..\..\..\build\Rx.props" />
<Import Project="..\..\..\build\AndroidWorkarounds.props" /> <Import Project="..\..\..\build\AndroidWorkarounds.props" />
</Project> </Project>

18
src/Android/Avalonia.Android/RuntimeInfo.cs

@ -1,18 +0,0 @@
using Avalonia.Platform;
namespace Avalonia.Shared.PlatformSupport
{
internal partial class StandardRuntimePlatform
{
public RuntimePlatformInfo GetRuntimeInfo() => new RuntimePlatformInfo
{
IsCoreClr = false,
IsDesktop = false,
IsMobile = true,
IsDotNetFramework = false,
IsMono = true,
IsUnix = true,
OperatingSystem = OperatingSystemType.Android
};
}
}

5
src/Avalonia.Base/Platform/IRuntimePlatform.cs

@ -1,5 +1,4 @@
using System; using System;
using System.Reflection;
namespace Avalonia.Platform namespace Avalonia.Platform
{ {
@ -23,6 +22,7 @@ namespace Avalonia.Platform
public OperatingSystemType OperatingSystem { get; set; } public OperatingSystemType OperatingSystem { get; set; }
public bool IsDesktop { get; set; } public bool IsDesktop { get; set; }
public bool IsMobile { get; set; } public bool IsMobile { get; set; }
public bool IsBrowser { get; set; }
public bool IsCoreClr { get; set; } public bool IsCoreClr { get; set; }
public bool IsMono { get; set; } public bool IsMono { get; set; }
public bool IsDotNetFramework { get; set; } public bool IsDotNetFramework { get; set; }
@ -36,6 +36,7 @@ namespace Avalonia.Platform
Linux, Linux,
OSX, OSX,
Android, Android,
iOS iOS,
Browser
} }
} }

2
src/Avalonia.Base/Utilities/ImmutableReadOnlyListStructEnumerator.cs

@ -3,7 +3,7 @@ using System.Collections.Generic;
namespace Avalonia.Utilities namespace Avalonia.Utilities
{ {
public struct ImmutableReadOnlyListStructEnumerator<T> : IEnumerator, IEnumerator<T> public struct ImmutableReadOnlyListStructEnumerator<T> : IEnumerator<T>
{ {
private readonly IReadOnlyList<T> _readOnlyList; private readonly IReadOnlyList<T> _readOnlyList;
private int _pos; private int _pos;

6
src/Avalonia.Controls/ApiCompatBaseline.txt

@ -43,6 +43,10 @@ MembersMustExist : Member 'public void Avalonia.Controls.Embedding.Offscreen.Off
MembersMustExist : Member 'public void Avalonia.Controls.Embedding.Offscreen.OffscreenTopLevelImplBase.SetCursor(Avalonia.Platform.IPlatformHandle)' does not exist in the implementation but it does exist in the contract. MembersMustExist : Member 'public void Avalonia.Controls.Embedding.Offscreen.OffscreenTopLevelImplBase.SetCursor(Avalonia.Platform.IPlatformHandle)' does not exist in the implementation but it does exist in the contract.
MembersMustExist : Member 'public Avalonia.AvaloniaProperty Avalonia.AvaloniaProperty Avalonia.Controls.Notifications.NotificationCard.CloseOnClickProperty' does not exist in the implementation but it does exist in the contract. MembersMustExist : Member 'public Avalonia.AvaloniaProperty Avalonia.AvaloniaProperty Avalonia.Controls.Notifications.NotificationCard.CloseOnClickProperty' does not exist in the implementation but it does exist in the contract.
InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Controls.Platform.ITopLevelNativeMenuExporter.SetNativeMenu(Avalonia.Controls.NativeMenu)' is present in the contract but not in the implementation. InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Controls.Platform.ITopLevelNativeMenuExporter.SetNativeMenu(Avalonia.Controls.NativeMenu)' is present in the contract but not in the implementation.
MembersMustExist : Member 'protected Avalonia.Media.FormattedText Avalonia.Controls.Presenters.TextPresenter.CreateFormattedText()' does not exist in the implementation but it does exist in the contract.
MembersMustExist : Member 'public Avalonia.Media.FormattedText Avalonia.Controls.Presenters.TextPresenter.FormattedText.get()' does not exist in the implementation but it does exist in the contract.
MembersMustExist : Member 'public System.Int32 Avalonia.Controls.Presenters.TextPresenter.GetCaretIndex(Avalonia.Point)' does not exist in the implementation but it does exist in the contract.
MembersMustExist : Member 'protected void Avalonia.Controls.Presenters.TextPresenter.InvalidateFormattedText()' does not exist in the implementation but it does exist in the contract.
CannotRemoveBaseTypeOrInterface : Type 'Avalonia.Controls.Primitives.PopupRoot' does not implement interface 'Avalonia.Utilities.IWeakSubscriber<Avalonia.Controls.ResourcesChangedEventArgs>' in the implementation but it does in the contract. CannotRemoveBaseTypeOrInterface : Type 'Avalonia.Controls.Primitives.PopupRoot' does not implement interface 'Avalonia.Utilities.IWeakSubscriber<Avalonia.Controls.ResourcesChangedEventArgs>' in the implementation but it does in the contract.
EnumValuesMustMatch : Enum value 'Avalonia.Platform.ExtendClientAreaChromeHints Avalonia.Platform.ExtendClientAreaChromeHints.Default' is (System.Int32)2 in the implementation but (System.Int32)1 in the contract. EnumValuesMustMatch : Enum value 'Avalonia.Platform.ExtendClientAreaChromeHints Avalonia.Platform.ExtendClientAreaChromeHints.Default' is (System.Int32)2 in the implementation but (System.Int32)1 in the contract.
InterfacesShouldHaveSameMembers : Interface member 'public System.Nullable<Avalonia.Size> Avalonia.Platform.ITopLevelImpl.FrameSize' is present in the implementation but not in the contract. InterfacesShouldHaveSameMembers : Interface member 'public System.Nullable<Avalonia.Size> Avalonia.Platform.ITopLevelImpl.FrameSize' is present in the implementation but not in the contract.
@ -63,4 +67,4 @@ InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Platfor
MembersMustExist : Member 'public void Avalonia.Platform.IWindowImpl.Resize(Avalonia.Size)' does not exist in the implementation but it does exist in the contract. MembersMustExist : Member 'public void Avalonia.Platform.IWindowImpl.Resize(Avalonia.Size)' does not exist in the implementation but it does exist in the contract.
InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Platform.IWindowImpl.Resize(Avalonia.Size, Avalonia.Platform.PlatformResizeReason)' is present in the implementation but not in the contract. InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Platform.IWindowImpl.Resize(Avalonia.Size, Avalonia.Platform.PlatformResizeReason)' is present in the implementation but not in the contract.
InterfacesShouldHaveSameMembers : Interface member 'public Avalonia.Platform.ITrayIconImpl Avalonia.Platform.IWindowingPlatform.CreateTrayIcon()' is present in the implementation but not in the contract. InterfacesShouldHaveSameMembers : Interface member 'public Avalonia.Platform.ITrayIconImpl Avalonia.Platform.IWindowingPlatform.CreateTrayIcon()' is present in the implementation but not in the contract.
Total Issues: 64 Total Issues: 68

19
src/Avalonia.Controls/Control.cs

@ -1,10 +1,12 @@
using System; using System;
using System.ComponentModel; using System.ComponentModel;
using System.Runtime.CompilerServices;
using Avalonia.Controls.Primitives; using Avalonia.Controls.Primitives;
using Avalonia.Controls.Templates; using Avalonia.Controls.Templates;
using Avalonia.Input; using Avalonia.Input;
using Avalonia.Input.Platform; using Avalonia.Input.Platform;
using Avalonia.Interactivity; using Avalonia.Interactivity;
using Avalonia.Media;
using Avalonia.Rendering; using Avalonia.Rendering;
using Avalonia.Styling; using Avalonia.Styling;
using Avalonia.VisualTree; using Avalonia.VisualTree;
@ -58,7 +60,13 @@ namespace Avalonia.Controls
public static readonly RoutedEvent<ContextRequestedEventArgs> ContextRequestedEvent = public static readonly RoutedEvent<ContextRequestedEventArgs> ContextRequestedEvent =
RoutedEvent.Register<Control, ContextRequestedEventArgs>(nameof(ContextRequested), RoutedEvent.Register<Control, ContextRequestedEventArgs>(nameof(ContextRequested),
RoutingStrategies.Tunnel | RoutingStrategies.Bubble); RoutingStrategies.Tunnel | RoutingStrategies.Bubble);
/// <summary>
/// Defines the <see cref="FlowDirection"/> property.
/// </summary>
public static readonly AttachedProperty<FlowDirection> FlowDirectionProperty =
AvaloniaProperty.RegisterAttached<Control, Control, FlowDirection>(nameof(FlowDirection), inherits: true);
private DataTemplates? _dataTemplates; private DataTemplates? _dataTemplates;
private IControl? _focusAdorner; private IControl? _focusAdorner;
@ -106,6 +114,15 @@ namespace Avalonia.Controls
get => GetValue(TagProperty); get => GetValue(TagProperty);
set => SetValue(TagProperty, value); set => SetValue(TagProperty, value);
} }
/// <summary>
/// Gets or sets the text flow direction.
/// </summary>
public FlowDirection FlowDirection
{
get => GetValue(FlowDirectionProperty);
set => SetValue(FlowDirectionProperty, value);
}
/// <summary> /// <summary>
/// Occurs when the user has completed a context input gesture, such as a right-click. /// Occurs when the user has completed a context input gesture, such as a right-click.

494
src/Avalonia.Controls/Presenters/TextPresenter.cs

@ -1,9 +1,12 @@
using System; using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Reactive.Linq; using System.Reactive.Linq;
using Avalonia.Input.TextInput;
using Avalonia.Media; using Avalonia.Media;
using Avalonia.Media.TextFormatting;
using Avalonia.Metadata; using Avalonia.Metadata;
using Avalonia.Threading; using Avalonia.Threading;
using Avalonia.Utilities;
using Avalonia.VisualTree; using Avalonia.VisualTree;
using Avalonia.Layout; using Avalonia.Layout;
using Avalonia.Media.Immutable; using Avalonia.Media.Immutable;
@ -75,28 +78,18 @@ namespace Avalonia.Controls.Presenters
private int _selectionEnd; private int _selectionEnd;
private bool _caretBlink; private bool _caretBlink;
private string _text; private string _text;
private FormattedText? _formattedText; private TextLayout _textLayout;
private Size _constraint; private Size _constraint = Size.Infinity;
static TextPresenter() private CharacterHit _lastCharacterHit;
{ private Rect _caretBounds;
AffectsRender<TextPresenter>(SelectionBrushProperty, TextBlock.ForegroundProperty, private Point _navigationPosition;
SelectionForegroundBrushProperty, CaretBrushProperty,
SelectionStartProperty, SelectionEndProperty);
AffectsMeasure<TextPresenter>(TextProperty, PasswordCharProperty, RevealPasswordProperty,
TextAlignmentProperty, TextWrappingProperty, TextBlock.FontSizeProperty,
TextBlock.FontStyleProperty, TextBlock.FontWeightProperty, TextBlock.FontFamilyProperty);
Observable.Merge<AvaloniaPropertyChangedEventArgs>(TextProperty.Changed, TextBlock.ForegroundProperty.Changed, private ScrollViewer _scrollViewer;
TextAlignmentProperty.Changed, TextWrappingProperty.Changed,
TextBlock.FontSizeProperty.Changed, TextBlock.FontStyleProperty.Changed,
TextBlock.FontWeightProperty.Changed, TextBlock.FontFamilyProperty.Changed,
SelectionStartProperty.Changed, SelectionEndProperty.Changed,
SelectionForegroundBrushProperty.Changed, PasswordCharProperty.Changed, RevealPasswordProperty.Changed
).AddClassHandler<TextPresenter>((x, _) => x.InvalidateFormattedText());
CaretIndexProperty.Changed.AddClassHandler<TextPresenter>((x, e) => x.CaretIndexChanged((int)e.NewValue!)); static TextPresenter()
{
AffectsRender<TextPresenter>(CaretBrushProperty, SelectionBrushProperty);
} }
public TextPresenter() public TextPresenter()
@ -106,6 +99,8 @@ namespace Avalonia.Controls.Presenters
_caretTimer.Tick += CaretTimerTick; _caretTimer.Tick += CaretTimerTick;
} }
public event EventHandler CaretBoundsChanged;
/// <summary> /// <summary>
/// Gets or sets a brush used to paint the control's background. /// Gets or sets a brush used to paint the control's background.
/// </summary> /// </summary>
@ -189,13 +184,22 @@ namespace Avalonia.Controls.Presenters
} }
/// <summary> /// <summary>
/// Gets the <see cref="FormattedText"/> used to render the text. /// Gets the <see cref="TextLayout"/> used to render the text.
/// </summary> /// </summary>
public FormattedText FormattedText public TextLayout TextLayout
{ {
get get
{ {
return _formattedText ?? (_formattedText = CreateFormattedText()); if (_textLayout != null)
{
return _textLayout;
}
_textLayout = CreateTextLayout();
UpdateCaret(_lastCharacterHit);
return _textLayout;
} }
} }
@ -205,11 +209,12 @@ namespace Avalonia.Controls.Presenters
{ {
return _caretIndex; return _caretIndex;
} }
set set
{ {
value = CoerceCaretIndex(value); if (value != _caretIndex)
SetAndRaise(CaretIndexProperty, ref _caretIndex, value); {
MoveCaretToTextPosition(value);
}
} }
} }
@ -271,37 +276,25 @@ namespace Avalonia.Controls.Presenters
} }
} }
public int GetCaretIndex(Point point)
{
var hit = FormattedText.HitTestPoint(point);
return hit.TextPosition + (hit.IsTrailing ? 1 : 0);
}
/// <summary> /// <summary>
/// Creates the <see cref="FormattedText"/> used to render the text. /// Creates the <see cref="TextLayout"/> used to render the text.
/// </summary> /// </summary>
/// <param name="constraint">The constraint of the text.</param> /// <param name="constraint">The constraint of the text.</param>
/// <param name="text">The text to format.</param> /// <param name="text">The text to format.</param>
/// <returns>A <see cref="FormattedText"/> object.</returns> /// <param name="typeface"></param>
private FormattedText CreateFormattedTextInternal(Size constraint, string text) /// <param name="textStyleOverrides"></param>
{ /// <returns>A <see cref="TextLayout"/> object.</returns>
return new FormattedText private TextLayout CreateTextLayoutInternal(Size constraint, string text, Typeface typeface,
{ IReadOnlyList<ValueSpan<TextRunProperties>> textStyleOverrides)
Constraint = constraint, {
Typeface = new Typeface(FontFamily, FontStyle, FontWeight), var maxWidth = MathUtilities.IsZero(constraint.Width) ? double.PositiveInfinity : constraint.Width;
FontSize = FontSize, var maxHeight = MathUtilities.IsZero(constraint.Height) ? double.PositiveInfinity : constraint.Height;
Text = text ?? string.Empty,
TextAlignment = TextAlignment, var textLayout = new TextLayout(text ?? string.Empty, typeface, FontSize, Foreground, TextAlignment,
TextWrapping = TextWrapping, TextWrapping, maxWidth: maxWidth, maxHeight: maxHeight, textStyleOverrides: textStyleOverrides,
}; flowDirection: FlowDirection);
}
/// <summary> return textLayout;
/// Invalidates <see cref="FormattedText"/>.
/// </summary>
protected void InvalidateFormattedText()
{
_formattedText = null;
} }
/// <summary> /// <summary>
@ -317,32 +310,36 @@ namespace Avalonia.Controls.Presenters
context.FillRectangle(background, new Rect(Bounds.Size)); context.FillRectangle(background, new Rect(Bounds.Size));
} }
double top = 0; var top = 0d;
var textSize = FormattedText.Bounds.Size; var left = 0.0;
var (_, textHeight) = TextLayout.Size;
if (Bounds.Height < textSize.Height) if (Bounds.Height < textHeight)
{ {
switch (VerticalAlignment) switch (VerticalAlignment)
{ {
case VerticalAlignment.Center: case VerticalAlignment.Center:
top += (Bounds.Height - textSize.Height) / 2; top += (Bounds.Height - textHeight) / 2;
break; break;
case VerticalAlignment.Bottom: case VerticalAlignment.Bottom:
top += (Bounds.Height - textSize.Height); top += (Bounds.Height - textHeight);
break; break;
} }
} }
if (Foreground is IBrush foreground) TextLayout.Draw(context, new Point(left, top));
context.DrawText(foreground, new Point(0, top), FormattedText);
} }
public override void Render(DrawingContext context) public override void Render(DrawingContext context)
{ {
FormattedText.Constraint = Bounds.Size; if (double.IsPositiveInfinity (_constraint.Width))
{
_constraint = Bounds.Size; _constraint = _scrollViewer?.Viewport ?? Size.Infinity;
InvalidateTextLayout();
}
var selectionStart = SelectionStart; var selectionStart = SelectionStart;
var selectionEnd = SelectionEnd; var selectionEnd = SelectionEnd;
@ -352,7 +349,7 @@ namespace Avalonia.Controls.Presenters
var start = Math.Min(selectionStart, selectionEnd); var start = Math.Min(selectionStart, selectionEnd);
var length = Math.Max(selectionStart, selectionEnd) - start; var length = Math.Max(selectionStart, selectionEnd) - start;
var rects = FormattedText.HitTestTextRange(start, length); var rects = TextLayout.HitTestTextRange(start, length);
foreach (var rect in rects) foreach (var rect in rects)
{ {
@ -362,40 +359,47 @@ namespace Avalonia.Controls.Presenters
RenderInternal(context); RenderInternal(context);
if (selectionStart == selectionEnd && _caretBlink) if (selectionStart != selectionEnd || !_caretBlink)
{
return;
}
var caretBrush = CaretBrush?.ToImmutable();
if (caretBrush is null)
{ {
var caretBrush = CaretBrush?.ToImmutable(); var backgroundColor = (Background as ISolidColorBrush)?.Color;
if (caretBrush is null) if (backgroundColor.HasValue)
{ {
var backgroundColor = (Background as ISolidColorBrush)?.Color; var red = (byte)~(backgroundColor.Value.R);
if (backgroundColor.HasValue) var green = (byte)~(backgroundColor.Value.G);
{ var blue = (byte)~(backgroundColor.Value.B);
byte red = (byte)~(backgroundColor.Value.R);
byte green = (byte)~(backgroundColor.Value.G);
byte blue = (byte)~(backgroundColor.Value.B);
caretBrush = new ImmutableSolidColorBrush(Color.FromRgb(red, green, blue)); caretBrush = new ImmutableSolidColorBrush(Color.FromRgb(red, green, blue));
} }
else else
{ {
caretBrush = Brushes.Black; caretBrush = Brushes.Black;
}
} }
var (p1, p2) = GetCaretPoints();
context.DrawLine(
new ImmutablePen(caretBrush, 1),
p1, p2);
} }
}
(Point, Point) GetCaretPoints() var (p1, p2) = GetCaretPoints();
context.DrawLine(new ImmutablePen(caretBrush), p1, p2);
}
private (Point, Point) GetCaretPoints()
{ {
var charPos = FormattedText.HitTestTextPosition(CaretIndex); var x = Math.Floor(_caretBounds.X) + 0.5;
var x = Math.Floor(charPos.X) + 0.5; var y = Math.Floor(_caretBounds.Y) + 0.5;
var y = Math.Floor(charPos.Y) + 0.5; var b = Math.Ceiling(_caretBounds.Bottom) - 0.5;
var b = Math.Ceiling(charPos.Bottom) - 0.5;
if (x >= Bounds.Width)
{
x = Math.Floor(_caretBounds.X - 1) + 0.5;
}
return (new Point(x, y), new Point(x, b)); return (new Point(x, y), new Point(x, b));
} }
@ -413,7 +417,7 @@ namespace Avalonia.Controls.Presenters
InvalidateVisual(); InvalidateVisual();
} }
internal void CaretIndexChanged(int caretIndex) internal void CaretChanged()
{ {
if (this.GetVisualParent() != null) if (this.GetVisualParent() != null)
{ {
@ -433,8 +437,7 @@ namespace Avalonia.Controls.Presenters
if (IsMeasureValid) if (IsMeasureValid)
{ {
var rect = FormattedText.HitTestTextPosition(caretIndex); this.BringIntoView(_caretBounds);
this.BringIntoView(rect);
} }
else else
{ {
@ -444,8 +447,7 @@ namespace Avalonia.Controls.Presenters
Dispatcher.UIThread.Post( Dispatcher.UIThread.Post(
() => () =>
{ {
var rect = FormattedText.HitTestTextPosition(caretIndex); this.BringIntoView(_caretBounds);
this.BringIntoView(rect);
}, },
DispatcherPriority.Render); DispatcherPriority.Render);
} }
@ -453,104 +455,310 @@ namespace Avalonia.Controls.Presenters
} }
/// <summary> /// <summary>
/// Creates the <see cref="FormattedText"/> used to render the text. /// Creates the <see cref="TextLayout"/> used to render the text.
/// </summary> /// </summary>
/// <returns>A <see cref="FormattedText"/> object.</returns> /// <returns>A <see cref="TextLayout"/> object.</returns>
protected virtual FormattedText CreateFormattedText() protected virtual TextLayout CreateTextLayout()
{ {
FormattedText? result = null; TextLayout result;
var text = Text; var text = Text;
if (PasswordChar != default(char) && !RevealPassword) var typeface = new Typeface(FontFamily, FontStyle, FontWeight);
{
result = CreateFormattedTextInternal(_constraint, new string(PasswordChar, text?.Length ?? 0));
}
else
{
result = CreateFormattedTextInternal(_constraint, text);
}
var selectionStart = SelectionStart; var selectionStart = SelectionStart;
var selectionEnd = SelectionEnd; var selectionEnd = SelectionEnd;
var start = Math.Min(selectionStart, selectionEnd); var start = Math.Min(selectionStart, selectionEnd);
var length = Math.Max(selectionStart, selectionEnd) - start; var length = Math.Max(selectionStart, selectionEnd) - start;
IReadOnlyList<ValueSpan<TextRunProperties>> textStyleOverrides = null;
if (length > 0) if (length > 0)
{ {
result.Spans = new[] textStyleOverrides = new[]
{ {
new FormattedTextStyleSpan(start, length, SelectionForegroundBrush), new ValueSpan<TextRunProperties>(start, length,
new GenericTextRunProperties(typeface, FontSize,
foregroundBrush: SelectionForegroundBrush ?? Brushes.White))
}; };
} }
if (PasswordChar != default(char) && !RevealPassword)
{
result = CreateTextLayoutInternal(_constraint, new string(PasswordChar, text?.Length ?? 0), typeface,
textStyleOverrides);
}
else
{
result = CreateTextLayoutInternal(_constraint, text, typeface, textStyleOverrides);
}
return result; return result;
} }
/// <summary> protected virtual void InvalidateTextLayout()
/// Measures the control. {
/// </summary> _textLayout = null;
/// <param name="availableSize">The available size for the control.</param>
/// <returns>The desired size.</returns> InvalidateMeasure();
private Size MeasureInternal(Size availableSize) }
protected override Size MeasureOverride(Size availableSize)
{
if (!double.IsInfinity(availableSize.Width) && availableSize != _constraint)
{
_constraint = availableSize;
InvalidateTextLayout();
}
return TextLayout.Size;
}
private int CoerceCaretIndex(int value)
{
var text = Text;
var length = text?.Length ?? 0;
return Math.Max(0, Math.Min(length, value));
}
private void CaretTimerTick(object sender, EventArgs e)
{
_caretBlink = !_caretBlink;
InvalidateVisual();
}
public void MoveCaretToTextPosition(int textPosition, bool trailingEdge = false)
{
var lineIndex = TextLayout.GetLineIndexFromCharacterIndex(textPosition, trailingEdge);
var textLine = TextLayout.TextLines[lineIndex];
var characterHit = textLine.GetPreviousCaretCharacterHit(new CharacterHit(textPosition));
var nextCaretCharacterHit = textLine.GetNextCaretCharacterHit(characterHit);
if (nextCaretCharacterHit.FirstCharacterIndex <= textPosition)
{
characterHit = nextCaretCharacterHit;
}
if (textPosition == characterHit.FirstCharacterIndex + characterHit.TrailingLength)
{
UpdateCaret(characterHit);
}
else
{
UpdateCaret(trailingEdge ? characterHit : new CharacterHit(characterHit.FirstCharacterIndex));
}
_navigationPosition = _caretBounds.Position;
}
public void MoveCaretToPoint(Point point)
{
var hit = TextLayout.HitTestPoint(point);
UpdateCaret(hit.CharacterHit);
_navigationPosition = _caretBounds.Position;
}
public void MoveCaretVertical(LogicalDirection direction = LogicalDirection.Forward)
{ {
if (!string.IsNullOrEmpty(Text)) var lineIndex = TextLayout.GetLineIndexFromCharacterIndex(CaretIndex, _lastCharacterHit.TrailingLength > 0);
if (lineIndex < 0)
{
return;
}
var (currentX, currentY) = _navigationPosition;
if (direction == LogicalDirection.Forward)
{ {
if (TextWrapping == TextWrapping.Wrap) if (lineIndex + 1 > TextLayout.TextLines.Count - 1)
{ {
_constraint = new Size(availableSize.Width, double.PositiveInfinity); return;
} }
else
var textLine = TextLayout.TextLines[lineIndex];
currentY += textLine.Height;
}
else
{
if (lineIndex - 1 < 0)
{ {
_constraint = Size.Infinity; return;
} }
_formattedText = null; var textLine = TextLayout.TextLines[--lineIndex];
return FormattedText.Bounds.Size; currentY -= textLine.Height;
} }
return new Size(); var navigationPosition = _navigationPosition;
MoveCaretToPoint(new Point(currentX, currentY));
_navigationPosition = navigationPosition.WithY(_caretBounds.Y);
} }
protected override Size MeasureOverride(Size availableSize) public void MoveCaretHorizontal(LogicalDirection direction = LogicalDirection.Forward)
{ {
var text = Text; var characterHit = _lastCharacterHit;
var caretIndex = characterHit.FirstCharacterIndex + characterHit.TrailingLength;
var lineIndex = TextLayout.GetLineIndexFromCharacterIndex(caretIndex, false);
if (!string.IsNullOrEmpty(text)) if (lineIndex < 0)
{ {
return MeasureInternal(availableSize); return;
}
if (direction == LogicalDirection.Forward)
{
while (lineIndex < TextLayout.TextLines.Count)
{
var textLine = TextLayout.TextLines[lineIndex];
characterHit = textLine.GetNextCaretCharacterHit(characterHit);
caretIndex = characterHit.FirstCharacterIndex + characterHit.TrailingLength;
if (textLine.NewLineLength > 0 && caretIndex == textLine.TextRange.Start + textLine.TextRange.Length)
{
characterHit = new CharacterHit(caretIndex);
}
if (caretIndex >= Text.Length)
{
characterHit = new CharacterHit(Text.Length);
break;
}
if (caretIndex - textLine.NewLineLength == textLine.TextRange.Start + textLine.TextRange.Length)
{
break;
}
if (caretIndex <= CaretIndex)
{
lineIndex++;
continue;
}
break;
}
} }
else else
{ {
return new FormattedText while (lineIndex >= 0)
{ {
Text = "X", var textLine = TextLayout.TextLines[lineIndex];
Typeface = new Typeface(FontFamily, FontStyle, FontWeight),
FontSize = FontSize, characterHit = textLine.GetPreviousCaretCharacterHit(characterHit);
TextAlignment = TextAlignment,
Constraint = availableSize, caretIndex = characterHit.FirstCharacterIndex + characterHit.TrailingLength;
}.Bounds.Size;
if (caretIndex >= CaretIndex)
{
lineIndex--;
continue;
}
break;
}
} }
UpdateCaret(characterHit);
_navigationPosition = _caretBounds.Position;
} }
private int CoerceCaretIndex(int value) private void UpdateCaret(CharacterHit characterHit)
{ {
var text = Text; _lastCharacterHit = characterHit;
var length = text?.Length ?? 0;
return Math.Max(0, Math.Min(length, value)); var caretIndex = characterHit.FirstCharacterIndex + characterHit.TrailingLength;
var lineIndex = TextLayout.GetLineIndexFromCharacterIndex(caretIndex, characterHit.TrailingLength > 0);
var textLine = TextLayout.TextLines[lineIndex];
var distanceX = textLine.GetDistanceFromCharacterHit(characterHit);
var distanceY = 0d;
for (var i = 0; i < lineIndex; i++)
{
var currentLine = TextLayout.TextLines[i];
distanceY += currentLine.Height;
}
var caretBounds = new Rect(distanceX, distanceY, 0, textLine.Height);
if (caretBounds != _caretBounds)
{
_caretBounds = caretBounds;
CaretBoundsChanged?.Invoke(this, EventArgs.Empty);
}
CaretChanged();
SetAndRaise(CaretIndexProperty, ref _caretIndex, caretIndex);
} }
private void CaretTimerTick(object? sender, EventArgs e) internal Rect GetCursorRectangle()
{ {
_caretBlink = !_caretBlink; return _caretBounds;
InvalidateVisual();
} }
internal Rect GetCursorRectangle() protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
{ {
var (p1, p2) = GetCaretPoints(); base.OnAttachedToVisualTree(e);
return new Rect(p1, p2);
_scrollViewer = this.FindAncestorOfType<ScrollViewer>();
}
protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e)
{
base.OnDetachedFromVisualTree(e);
_scrollViewer = null;
_caretTimer.Stop();
_caretTimer.Tick -= CaretTimerTick;
}
protected override void OnPropertyChanged<T>(AvaloniaPropertyChangedEventArgs<T> change)
{
base.OnPropertyChanged(change);
switch (change.Property.Name)
{
case nameof (TextBlock.Foreground):
case nameof (TextBlock.FontSize):
case nameof (TextBlock.FontStyle):
case nameof (TextBlock.FontWeight):
case nameof (TextBlock.FontFamily):
case nameof (Text):
case nameof (TextAlignment):
case nameof (TextWrapping):
case nameof (SelectionStart):
case nameof (SelectionEnd):
case nameof (SelectionForegroundBrush):
case nameof (PasswordChar):
case nameof (RevealPassword):
{
InvalidateTextLayout();
break;
}
}
} }
} }
} }

6
src/Avalonia.Controls/Primitives/PopupRoot.cs

@ -73,7 +73,11 @@ namespace Avalonia.Controls.Primitives
IStyleHost? IStyleHost.StylingParent => Parent; IStyleHost? IStyleHost.StylingParent => Parent;
/// <inheritdoc/> /// <inheritdoc/>
public void Dispose() => PlatformImpl?.Dispose(); public void Dispose()
{
PlatformImpl?.Dispose();
HandleClosed();
}
private void UpdatePosition() private void UpdatePosition()
{ {

1
src/Avalonia.Controls/Properties/AssemblyInfo.cs

@ -3,6 +3,7 @@ using Avalonia.Metadata;
[assembly: InternalsVisibleTo("Avalonia.Controls.UnitTests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c1bba1142285fe0419326fb25866ba62c47e6c2b5c1ab0c95b46413fad375471232cb81706932e1cef38781b9ebd39d5100401bacb651c6c5bbf59e571e81b3bc08d2a622004e08b1a6ece82a7e0b9857525c86d2b95fab4bc3dce148558d7f3ae61aa3a234086902aeface87d9dfdd32b9d2fe3c6dd4055b5ab4b104998bd87")] [assembly: InternalsVisibleTo("Avalonia.Controls.UnitTests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c1bba1142285fe0419326fb25866ba62c47e6c2b5c1ab0c95b46413fad375471232cb81706932e1cef38781b9ebd39d5100401bacb651c6c5bbf59e571e81b3bc08d2a622004e08b1a6ece82a7e0b9857525c86d2b95fab4bc3dce148558d7f3ae61aa3a234086902aeface87d9dfdd32b9d2fe3c6dd4055b5ab4b104998bd87")]
[assembly: InternalsVisibleTo("Avalonia.DesignerSupport, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c1bba1142285fe0419326fb25866ba62c47e6c2b5c1ab0c95b46413fad375471232cb81706932e1cef38781b9ebd39d5100401bacb651c6c5bbf59e571e81b3bc08d2a622004e08b1a6ece82a7e0b9857525c86d2b95fab4bc3dce148558d7f3ae61aa3a234086902aeface87d9dfdd32b9d2fe3c6dd4055b5ab4b104998bd87")] [assembly: InternalsVisibleTo("Avalonia.DesignerSupport, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c1bba1142285fe0419326fb25866ba62c47e6c2b5c1ab0c95b46413fad375471232cb81706932e1cef38781b9ebd39d5100401bacb651c6c5bbf59e571e81b3bc08d2a622004e08b1a6ece82a7e0b9857525c86d2b95fab4bc3dce148558d7f3ae61aa3a234086902aeface87d9dfdd32b9d2fe3c6dd4055b5ab4b104998bd87")]
[assembly: InternalsVisibleTo("Avalonia.LeakTests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c1bba1142285fe0419326fb25866ba62c47e6c2b5c1ab0c95b46413fad375471232cb81706932e1cef38781b9ebd39d5100401bacb651c6c5bbf59e571e81b3bc08d2a622004e08b1a6ece82a7e0b9857525c86d2b95fab4bc3dce148558d7f3ae61aa3a234086902aeface87d9dfdd32b9d2fe3c6dd4055b5ab4b104998bd87")]
[assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia")] [assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia")]
[assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Controls")] [assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Controls")]

52
src/Avalonia.Controls/TextBlock.cs

@ -131,21 +131,8 @@ namespace Avalonia.Controls
static TextBlock() static TextBlock()
{ {
ClipToBoundsProperty.OverrideDefaultValue<TextBlock>(true); ClipToBoundsProperty.OverrideDefaultValue<TextBlock>(true);
AffectsRender<TextBlock>(BackgroundProperty, ForegroundProperty, AffectsRender<TextBlock>(BackgroundProperty, ForegroundProperty);
TextAlignmentProperty, TextDecorationsProperty);
AffectsMeasure<TextBlock>(FontSizeProperty, FontWeightProperty,
FontStyleProperty, TextWrappingProperty, FontFamilyProperty,
TextTrimmingProperty, TextProperty, PaddingProperty, LineHeightProperty, MaxLinesProperty);
Observable.Merge<AvaloniaPropertyChangedEventArgs>(TextProperty.Changed, ForegroundProperty.Changed,
TextAlignmentProperty.Changed, TextWrappingProperty.Changed,
TextTrimmingProperty.Changed, FontSizeProperty.Changed,
FontStyleProperty.Changed, FontWeightProperty.Changed,
FontFamilyProperty.Changed, TextDecorationsProperty.Changed,
PaddingProperty.Changed, MaxLinesProperty.Changed, LineHeightProperty.Changed
).AddClassHandler<TextBlock>((x, _) => x.InvalidateTextLayout());
} }
/// <summary> /// <summary>
@ -460,6 +447,7 @@ namespace Avalonia.Controls
TextWrapping, TextWrapping,
TextTrimming, TextTrimming,
TextDecorations, TextDecorations,
FlowDirection,
constraint.Width, constraint.Width,
constraint.Height, constraint.Height,
maxLines: MaxLines, maxLines: MaxLines,
@ -472,6 +460,8 @@ namespace Avalonia.Controls
protected void InvalidateTextLayout() protected void InvalidateTextLayout()
{ {
_textLayout = null; _textLayout = null;
InvalidateMeasure();
} }
/// <summary> /// <summary>
@ -507,12 +497,40 @@ namespace Avalonia.Controls
base.OnAttachedToLogicalTree(e); base.OnAttachedToLogicalTree(e);
InvalidateTextLayout(); InvalidateTextLayout();
InvalidateMeasure();
} }
private static bool IsValidMaxLines(int maxLines) => maxLines >= 0; private static bool IsValidMaxLines(int maxLines) => maxLines >= 0;
private static bool IsValidLineHeight(double lineHeight) => double.IsNaN(lineHeight) || lineHeight > 0; private static bool IsValidLineHeight(double lineHeight) => double.IsNaN(lineHeight) || lineHeight > 0;
protected override void OnPropertyChanged<T>(AvaloniaPropertyChangedEventArgs<T> change)
{
base.OnPropertyChanged(change);
switch (change.Property.Name)
{
case nameof (FontSize):
case nameof (FontWeight):
case nameof (FontStyle):
case nameof (FontFamily):
case nameof (TextWrapping):
case nameof (TextTrimming):
case nameof (TextAlignment):
case nameof (FlowDirection):
case nameof (Padding):
case nameof (LineHeight):
case nameof (MaxLines):
case nameof (Text):
case nameof (TextDecorations):
case nameof (Foreground):
{
InvalidateTextLayout();
break;
}
}
}
} }
} }

323
src/Avalonia.Controls/TextBox.cs

@ -14,6 +14,8 @@ using Avalonia.Data;
using Avalonia.Layout; using Avalonia.Layout;
using Avalonia.Utilities; using Avalonia.Utilities;
using Avalonia.Controls.Metadata; using Avalonia.Controls.Metadata;
using Avalonia.Media.TextFormatting;
using Avalonia.Media.TextFormatting.Unicode;
namespace Avalonia.Controls namespace Avalonia.Controls
{ {
@ -250,6 +252,7 @@ namespace Avalonia.Controls
{ {
value = CoerceCaretIndex(value); value = CoerceCaretIndex(value);
SetAndRaise(CaretIndexProperty, ref _caretIndex, value); SetAndRaise(CaretIndexProperty, ref _caretIndex, value);
UndoRedoState state; UndoRedoState state;
if (IsUndoEnabled && _undoRedoHelper.TryGetLastState(out state) && state.Text == Text) if (IsUndoEnabled && _undoRedoHelper.TryGetLastState(out state) && state.Text == Text)
_undoRedoHelper.UpdateLastState(); _undoRedoHelper.UpdateLastState();
@ -301,7 +304,8 @@ namespace Avalonia.Controls
{ {
UpdateCommandStates(); UpdateCommandStates();
} }
if (SelectionStart == SelectionEnd)
if (value == SelectionEnd)
{ {
CaretIndex = SelectionStart; CaretIndex = SelectionStart;
} }
@ -319,13 +323,15 @@ namespace Avalonia.Controls
{ {
value = CoerceCaretIndex(value); value = CoerceCaretIndex(value);
var changed = SetAndRaise(SelectionEndProperty, ref _selectionEnd, value); var changed = SetAndRaise(SelectionEndProperty, ref _selectionEnd, value);
if (changed) if (changed)
{ {
UpdateCommandStates(); UpdateCommandStates();
} }
if (SelectionStart == SelectionEnd)
if (value == SelectionStart)
{ {
CaretIndex = SelectionEnd; CaretIndex = value;
} }
} }
} }
@ -345,6 +351,7 @@ namespace Avalonia.Controls
if (!_ignoreTextChanges) if (!_ignoreTextChanges)
{ {
var caretIndex = CaretIndex; var caretIndex = CaretIndex;
SelectionStart = CoerceCaretIndex(SelectionStart, value); SelectionStart = CoerceCaretIndex(SelectionStart, value);
SelectionEnd = CoerceCaretIndex(SelectionEnd, value); SelectionEnd = CoerceCaretIndex(SelectionEnd, value);
CaretIndex = CoerceCaretIndex(caretIndex, value); CaretIndex = CoerceCaretIndex(caretIndex, value);
@ -533,13 +540,27 @@ namespace Avalonia.Controls
protected override void OnApplyTemplate(TemplateAppliedEventArgs e) protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
{ {
_presenter = e.NameScope.Get<TextPresenter>("PART_TextPresenter"); _presenter = e.NameScope.Get<TextPresenter>("PART_TextPresenter");
}
protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
{
base.OnAttachedToVisualTree(e);
_imClient.SetPresenter(_presenter, this); _imClient.SetPresenter(_presenter, this);
if (IsFocused) if (IsFocused)
{ {
_presenter?.ShowCaret(); _presenter?.ShowCaret();
} }
} }
protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e)
{
base.OnDetachedFromVisualTree(e);
_imClient.SetPresenter(null, null);
}
protected override void OnPropertyChanged<T>(AvaloniaPropertyChangedEventArgs<T> change) protected override void OnPropertyChanged<T>(AvaloniaPropertyChangedEventArgs<T> change)
{ {
base.OnPropertyChanged(change); base.OnPropertyChanged(change);
@ -631,9 +652,8 @@ namespace Avalonia.Controls
_selectedTextChangesMadeSinceLastUndoSnapshot++; _selectedTextChangesMadeSinceLastUndoSnapshot++;
SnapshotUndoRedo(ignoreChangeCount: false); SnapshotUndoRedo(ignoreChangeCount: false);
string text = Text ?? string.Empty; var text = Text ?? string.Empty;
int caretIndex = CaretIndex; var newLength = input.Length + text.Length - Math.Abs(SelectionStart - SelectionEnd);
int newLength = input.Length + text.Length - Math.Abs(SelectionStart - SelectionEnd);
if (MaxLength > 0 && newLength > MaxLength) if (MaxLength > 0 && newLength > MaxLength)
{ {
@ -649,11 +669,11 @@ namespace Avalonia.Controls
try try
{ {
DeleteSelection(false); DeleteSelection(false);
caretIndex = CaretIndex; var caretIndex = CaretIndex;
text = Text ?? string.Empty; text = Text ?? string.Empty;
SetTextInternal(text.Substring(0, caretIndex) + input + text.Substring(caretIndex)); SetTextInternal(text.Substring(0, caretIndex) + input + text.Substring(caretIndex));
CaretIndex += input.Length;
ClearSelection(); ClearSelection();
if (IsUndoEnabled) if (IsUndoEnabled)
{ {
_undoRedoHelper.DiscardRedo(); _undoRedoHelper.DiscardRedo();
@ -663,6 +683,8 @@ namespace Avalonia.Controls
{ {
RaisePropertyChanged(TextProperty, oldText, _text); RaisePropertyChanged(TextProperty, oldText, _text);
} }
CaretIndex = caretIndex + input.Length;
} }
finally finally
{ {
@ -687,6 +709,7 @@ namespace Avalonia.Controls
public async void Cut() public async void Cut()
{ {
var text = GetSelection(); var text = GetSelection();
if (string.IsNullOrEmpty(text)) if (string.IsNullOrEmpty(text))
{ {
return; return;
@ -706,6 +729,7 @@ namespace Avalonia.Controls
public async void Copy() public async void Copy()
{ {
var text = GetSelection(); var text = GetSelection();
if (string.IsNullOrEmpty(text)) if (string.IsNullOrEmpty(text))
{ {
return; return;
@ -742,11 +766,16 @@ namespace Avalonia.Controls
protected override void OnKeyDown(KeyEventArgs e) protected override void OnKeyDown(KeyEventArgs e)
{ {
string text = Text ?? string.Empty; if (_presenter == null)
int caretIndex = CaretIndex; {
bool movement = false; return;
bool selection = false; }
bool handled = false;
var text = Text ?? string.Empty;
var caretIndex = CaretIndex;
var movement = false;
var selection = false;
var handled = false;
var modifiers = e.KeyModifiers; var modifiers = e.KeyModifiers;
var keymap = AvaloniaLocator.Current.GetRequiredService<PlatformHotkeyConfiguration>(); var keymap = AvaloniaLocator.Current.GetRequiredService<PlatformHotkeyConfiguration>();
@ -887,46 +916,85 @@ namespace Avalonia.Controls
break; break;
case Key.Up: case Key.Up:
movement = MoveVertical(-1); {
selection = DetectSelection(); selection = DetectSelection();
break;
_presenter.MoveCaretVertical(LogicalDirection.Backward);
if (caretIndex != _presenter.CaretIndex)
{
movement = true;
}
if (selection)
{
SelectionEnd = _presenter.CaretIndex;
}
break;
}
case Key.Down: case Key.Down:
movement = MoveVertical(1); {
selection = DetectSelection(); selection = DetectSelection();
_presenter?.MoveCaretVertical();
if (caretIndex != _presenter.CaretIndex)
{
movement = true;
}
if (selection)
{
SelectionEnd = _presenter.CaretIndex;
}
break; break;
}
case Key.Back: case Key.Back:
{
SnapshotUndoRedo(); SnapshotUndoRedo();
if (hasWholeWordModifiers && SelectionStart == SelectionEnd) if (hasWholeWordModifiers && SelectionStart == SelectionEnd)
{ {
SetSelectionForControlBackspace(); SetSelectionForControlBackspace();
} }
if (!DeleteSelection() && CaretIndex > 0) if (!DeleteSelection() && caretIndex > 0)
{ {
var removedCharacters = 1; var removedCharacters = 0;
// handle deleting /r/n
// you don't ever want to leave a dangling /r around. So, if deleting /n, check to see if // \r\n needs special treatment here
// a /r should also be deleted. if (caretIndex - 1 > 0 && text[caretIndex - 1] == '\n' && text[caretIndex - 2] == '\r')
if (CaretIndex > 1 &&
text[CaretIndex - 1] == '\n' &&
text[CaretIndex - 2] == '\r')
{ {
removedCharacters = 2; removedCharacters = 2;
} }
else
{
Codepoint.ReadAt(text.AsMemory(), caretIndex - 1, out removedCharacters);
}
if (removedCharacters == 0)
{
return;
}
SetTextInternal(text.Substring(0, caretIndex - removedCharacters) + var length = Math.Max(0, caretIndex - removedCharacters);
SetTextInternal(text.Substring(0, length) +
text.Substring(caretIndex)); text.Substring(caretIndex));
CaretIndex -= removedCharacters;
CaretIndex = caretIndex - removedCharacters;
ClearSelection(); ClearSelection();
} }
handled = true; handled = true;
break; break;
}
case Key.Delete: case Key.Delete:
SnapshotUndoRedo(); SnapshotUndoRedo();
if (hasWholeWordModifiers && SelectionStart == SelectionEnd) if (hasWholeWordModifiers && SelectionStart == SelectionEnd)
{ {
SetSelectionForControlDelete(); SetSelectionForControlDelete();
@ -934,21 +1002,18 @@ namespace Avalonia.Controls
if (!DeleteSelection() && caretIndex < text.Length) if (!DeleteSelection() && caretIndex < text.Length)
{ {
var removedCharacters = 1; _presenter.MoveCaretHorizontal();
// handle deleting /r/n
// you don't ever want to leave a dangling /r around. So, if deleting /n, check to see if var removedCharacters = Math.Max(0, _presenter.CaretIndex - caretIndex);
// a /r should also be deleted.
if (CaretIndex < text.Length - 1 &&
text[caretIndex + 1] == '\n' &&
text[caretIndex] == '\r')
{
removedCharacters = 2;
}
SetTextInternal(text.Substring(0, caretIndex) + SetTextInternal(text.Substring(0, caretIndex) +
text.Substring(caretIndex + removedCharacters)); text.Substring(caretIndex + removedCharacters));
CaretIndex = caretIndex;
} }
SnapshotUndoRedo();
handled = true; handled = true;
break; break;
@ -986,11 +1051,7 @@ namespace Avalonia.Controls
} }
} }
if (movement && selection) if (movement && !selection)
{
SelectionEnd = CaretIndex;
}
else if (movement)
{ {
ClearSelection(); ClearSelection();
} }
@ -1003,19 +1064,28 @@ namespace Avalonia.Controls
protected override void OnPointerPressed(PointerPressedEventArgs e) protected override void OnPointerPressed(PointerPressedEventArgs e)
{ {
if (_presenter == null)
{
return;
}
var text = Text; var text = Text;
var clickInfo = e.GetCurrentPoint(this); var clickInfo = e.GetCurrentPoint(this);
if (text != null && clickInfo.Properties.IsLeftButtonPressed && !(clickInfo.Pointer?.Captured is Border))
if (text != null && clickInfo.Properties.IsLeftButtonPressed &&
!(clickInfo.Pointer?.Captured is Border))
{ {
var point = e.GetPosition(_presenter); var point = e.GetPosition(_presenter);
var index = _presenter!.GetCaretIndex(point);
var clickToSelect = index != CaretIndex && e.KeyModifiers.HasFlag(KeyModifiers.Shift);
if (!clickToSelect) var oldIndex = CaretIndex;
{
CaretIndex = index; _presenter.MoveCaretToPoint(point);
}
var index = _presenter.CaretIndex;
var clickToSelect = e.KeyModifiers.HasFlag(KeyModifiers.Shift);
SetAndRaise(CaretIndexProperty, ref _caretIndex, index);
#pragma warning disable CS0618 // Type or member is obsolete #pragma warning disable CS0618 // Type or member is obsolete
switch (e.ClickCount) switch (e.ClickCount)
@ -1024,13 +1094,14 @@ namespace Avalonia.Controls
case 1: case 1:
if (clickToSelect) if (clickToSelect)
{ {
SelectionStart = Math.Min(index, CaretIndex); SelectionStart = Math.Min(oldIndex, index);
SelectionEnd = Math.Max(index, CaretIndex); SelectionEnd = Math.Max(oldIndex, index);
} }
else else
{ {
SelectionStart = SelectionEnd = index; SelectionStart = SelectionEnd = index;
} }
break; break;
case 2: case 2:
if (!StringUtils.IsStartOfWord(text, index)) if (!StringUtils.IsStartOfWord(text, index))
@ -1052,8 +1123,13 @@ namespace Avalonia.Controls
protected override void OnPointerMoved(PointerEventArgs e) protected override void OnPointerMoved(PointerEventArgs e)
{ {
if (_presenter == null)
{
return;
}
// selection should not change during pointer move if the user right clicks // selection should not change during pointer move if the user right clicks
if (_presenter != null && e.Pointer.Captured == _presenter && e.GetCurrentPoint(this).Properties.IsLeftButtonPressed) if (e.Pointer.Captured == _presenter && e.GetCurrentPoint(this).Properties.IsLeftButtonPressed)
{ {
var point = e.GetPosition(_presenter); var point = e.GetPosition(_presenter);
@ -1061,32 +1137,45 @@ namespace Avalonia.Controls
MathUtilities.Clamp(point.X, 0, Math.Max(_presenter.Bounds.Width - 1, 0)), MathUtilities.Clamp(point.X, 0, Math.Max(_presenter.Bounds.Width - 1, 0)),
MathUtilities.Clamp(point.Y, 0, Math.Max(_presenter.Bounds.Height - 1, 0))); MathUtilities.Clamp(point.Y, 0, Math.Max(_presenter.Bounds.Height - 1, 0)));
CaretIndex = SelectionEnd = _presenter.GetCaretIndex(point); _presenter.MoveCaretToPoint(point);
SelectionEnd = _presenter.CaretIndex;
} }
} }
protected override void OnPointerReleased(PointerReleasedEventArgs e) protected override void OnPointerReleased(PointerReleasedEventArgs e)
{ {
if (_presenter != null && e.Pointer.Captured == _presenter) if (_presenter == null)
{
return;
}
if (e.Pointer.Captured != _presenter)
{
return;
}
if (e.InitialPressMouseButton == MouseButton.Right)
{ {
if (e.InitialPressMouseButton == MouseButton.Right) var point = e.GetPosition(_presenter);
_presenter.MoveCaretToPoint(point);
var caretIndex = _presenter.CaretIndex;
// see if mouse clicked inside current selection
// if it did not, we change the selection to where the user clicked
var firstSelection = Math.Min(SelectionStart, SelectionEnd);
var lastSelection = Math.Max(SelectionStart, SelectionEnd);
var didClickInSelection = SelectionStart != SelectionEnd &&
caretIndex >= firstSelection && caretIndex <= lastSelection;
if (!didClickInSelection)
{ {
var point = e.GetPosition(_presenter); CaretIndex = SelectionEnd = SelectionStart = caretIndex;
var caretIndex = _presenter.GetCaretIndex(point);
// see if mouse clicked inside current selection
// if it did not, we change the selection to where the user clicked
var firstSelection = Math.Min(SelectionStart, SelectionEnd);
var lastSelection = Math.Max(SelectionStart, SelectionEnd);
var didClickInSelection = SelectionStart != SelectionEnd &&
caretIndex >= firstSelection && caretIndex <= lastSelection;
if (!didClickInSelection)
{
CaretIndex = SelectionEnd = SelectionStart = caretIndex;
}
} }
e.Pointer.Capture(null);
} }
e.Pointer.Capture(null);
} }
protected override void UpdateDataValidation<T>(AvaloniaProperty<T> property, BindingValue<T> value) protected override void UpdateDataValidation<T>(AvaloniaProperty<T> property, BindingValue<T> value)
@ -1133,80 +1222,43 @@ namespace Avalonia.Controls
private void MoveHorizontal(int direction, bool wholeWord, bool isSelecting) private void MoveHorizontal(int direction, bool wholeWord, bool isSelecting)
{ {
var text = Text ?? string.Empty; var text = Text ?? string.Empty;
var caretIndex = CaretIndex; var selectionStart = SelectionStart;
if (!wholeWord) if (!wholeWord)
{ {
if (SelectionStart != SelectionEnd && !isSelecting) if (_presenter == null)
{ {
var start = Math.Min(SelectionStart, SelectionEnd);
var end = Math.Max(SelectionStart, SelectionEnd);
CaretIndex = direction < 0 ? start : end;
return; return;
} }
_presenter.MoveCaretHorizontal(direction > 0 ? LogicalDirection.Forward : LogicalDirection.Backward);
var index = caretIndex + direction; if (isSelecting)
if (index < 0 || index > text.Length)
{ {
return; SelectionEnd = _presenter.CaretIndex;
}
else if (index == text.Length)
{
CaretIndex = index;
return;
}
var c = text[index];
if (direction > 0)
{
CaretIndex += (c == '\r' && index < text.Length - 1 && text[index + 1] == '\n') ? 2 : 1;
} }
else else
{ {
CaretIndex -= (c == '\n' && index > 0 && text[index - 1] == '\r') ? 2 : 1; SelectionStart = SelectionEnd = _presenter.CaretIndex;
} }
} }
else else
{ {
if (direction > 0) if (direction > 0)
{ {
CaretIndex += StringUtils.NextWord(text, caretIndex) - caretIndex; var offset = StringUtils.NextWord(text, selectionStart) - selectionStart;
CaretIndex += offset;
} }
else else
{ {
CaretIndex += StringUtils.PreviousWord(text, caretIndex) - caretIndex; var offset = StringUtils.PreviousWord(text, selectionStart) - selectionStart;
CaretIndex += offset;
} }
} }
} }
private bool MoveVertical(int count)
{
if (_presenter is null)
{
return false;
}
var formattedText = _presenter.FormattedText;
var lines = formattedText.GetLines().ToList();
var caretIndex = CaretIndex;
var lineIndex = GetLine(caretIndex, lines) + count;
if (lineIndex >= 0 && lineIndex < lines.Count)
{
var line = lines[lineIndex];
var rect = formattedText.HitTestTextPosition(caretIndex);
var y = count < 0 ? rect.Y : rect.Bottom;
var point = new Point(rect.X, y + (count * (line.Height / 2)));
var hit = formattedText.HitTestPoint(point);
CaretIndex = hit.TextPosition + (hit.IsTrailing ? 1 : 0);
return true;
}
return false;
}
private void MoveHome(bool document) private void MoveHome(bool document)
{ {
if (_presenter is null) if (_presenter is null)
@ -1223,17 +1275,17 @@ namespace Avalonia.Controls
} }
else else
{ {
var lines = _presenter.FormattedText.GetLines(); var lines = _presenter.TextLayout.TextLines;
var pos = 0; var pos = 0;
foreach (var line in lines) foreach (var line in lines)
{ {
if (pos + line.Length > caretIndex || pos + line.Length == text.Length) if (pos + line.TextRange.Length > caretIndex || pos + line.TextRange.Length == text.Length)
{ {
break; break;
} }
pos += line.Length; pos += line.TextRange.Length;
} }
caretIndex = pos; caretIndex = pos;
@ -1258,12 +1310,12 @@ namespace Avalonia.Controls
} }
else else
{ {
var lines = _presenter.FormattedText.GetLines(); var lines = _presenter.TextLayout.TextLines;
var pos = 0; var pos = 0;
foreach (var line in lines) foreach (var line in lines)
{ {
pos += line.Length; pos += line.TextRange.Length;
if (pos > caretIndex) if (pos > caretIndex)
{ {
@ -1340,25 +1392,6 @@ namespace Avalonia.Controls
return text.Substring(start, end - start); return text.Substring(start, end - start);
} }
private int GetLine(int caretIndex, IList<FormattedTextLine> lines)
{
int pos = 0;
int i;
for (i = 0; i < lines.Count - 1; ++i)
{
var line = lines[i];
pos += line.Length;
if (pos > caretIndex)
{
break;
}
}
return i;
}
private void SetTextInternal(string value, bool raiseTextChanged = true) private void SetTextInternal(string value, bool raiseTextChanged = true)
{ {
if (raiseTextChanged) if (raiseTextChanged)

28
src/Avalonia.Controls/TextBoxTextInputMethodClient.cs

@ -1,4 +1,5 @@
using System; using System;
using System.Diagnostics;
using Avalonia.Controls.Presenters; using Avalonia.Controls.Presenters;
using Avalonia.Input; using Avalonia.Input;
using Avalonia.Input.TextInput; using Avalonia.Input.TextInput;
@ -8,9 +9,9 @@ namespace Avalonia.Controls
{ {
internal class TextBoxTextInputMethodClient : ITextInputMethodClient internal class TextBoxTextInputMethodClient : ITextInputMethodClient
{ {
private InputElement? _parent; private InputElement _parent;
private TextPresenter? _presenter; private TextPresenter _presenter;
private IDisposable? _subscription;
public Rect CursorRectangle public Rect CursorRectangle
{ {
get get
@ -20,11 +21,15 @@ namespace Avalonia.Controls
return default; return default;
} }
var transform = _presenter.TransformToVisual(_parent); var transform = _presenter.TransformToVisual(_parent);
if (transform == null) if (transform == null)
{ {
return default; return default;
} }
return _presenter.GetCursorRectangle().TransformToAABB(transform.Value);
var rect = _presenter.GetCursorRectangle().TransformToAABB(transform.Value);
return rect;
} }
} }
@ -40,20 +45,25 @@ namespace Avalonia.Controls
public string? TextBeforeCursor => null; public string? TextBeforeCursor => null;
public string? TextAfterCursor => null; public string? TextAfterCursor => null;
private void OnCaretIndexChanged(int index) => CursorRectangleChanged?.Invoke(this, EventArgs.Empty); private void OnCaretBoundsChanged(object sender, EventArgs e) => CursorRectangleChanged?.Invoke(this, EventArgs.Empty);
public void SetPresenter(TextPresenter presenter, InputElement parent) public void SetPresenter(TextPresenter presenter, InputElement parent)
{ {
_parent = parent; _parent = parent;
_subscription?.Dispose();
_subscription = null; if (_presenter != null)
{
_presenter.CaretBoundsChanged -= OnCaretBoundsChanged;
}
_presenter = presenter; _presenter = presenter;
if (_presenter != null) if (_presenter != null)
{ {
_subscription = _presenter.GetObservable(TextPresenter.CaretIndexProperty) _presenter.CaretBoundsChanged += OnCaretBoundsChanged;
.Subscribe(OnCaretIndexChanged);
} }
TextViewVisualChanged?.Invoke(this, EventArgs.Empty); TextViewVisualChanged?.Invoke(this, EventArgs.Empty);
CursorRectangleChanged?.Invoke(this, EventArgs.Empty); CursorRectangleChanged?.Invoke(this, EventArgs.Empty);
} }

53
src/Avalonia.Controls/Utils/StringUtils.cs

@ -1,4 +1,5 @@
using System.Globalization; using System.Globalization;
using Avalonia.Media.TextFormatting.Unicode;
namespace Avalonia.Controls.Utils namespace Avalonia.Controls.Utils
{ {
@ -23,26 +24,38 @@ namespace Avalonia.Controls.Utils
return false; return false;
} }
var codepoint = new Codepoint(text[index]);
// A 'word' starts with an AlphaNumeric or some punctuation symbols immediately // A 'word' starts with an AlphaNumeric or some punctuation symbols immediately
// preceeded by lwsp. // preceeded by lwsp.
if (index > 0 && !char.IsWhiteSpace(text[index - 1])) if (index > 0)
{ {
return false; var previousCodepoint = new Codepoint(text[index - 1]);
if (!previousCodepoint.IsWhiteSpace)
{
return false;
}
if (previousCodepoint.IsBreakChar)
{
return true;
}
} }
switch (CharUnicodeInfo.GetUnicodeCategory(text[index])) switch (codepoint.GeneralCategory)
{ {
case UnicodeCategory.LowercaseLetter: case GeneralCategory.LowercaseLetter:
case UnicodeCategory.TitlecaseLetter: case GeneralCategory.TitlecaseLetter:
case UnicodeCategory.UppercaseLetter: case GeneralCategory.UppercaseLetter:
case UnicodeCategory.DecimalDigitNumber: case GeneralCategory.DecimalNumber:
case UnicodeCategory.LetterNumber: case GeneralCategory.LetterNumber:
case UnicodeCategory.OtherNumber: case GeneralCategory.OtherNumber:
case UnicodeCategory.DashPunctuation: case GeneralCategory.DashPunctuation:
case UnicodeCategory.InitialQuotePunctuation: case GeneralCategory.InitialPunctuation:
case UnicodeCategory.OpenPunctuation: case GeneralCategory.OpenPunctuation:
case UnicodeCategory.CurrencySymbol: case GeneralCategory.CurrencySymbol:
case UnicodeCategory.MathSymbol: case GeneralCategory.MathSymbol:
return true; return true;
// TODO: How do you do this in .NET? // TODO: How do you do this in .NET?
@ -56,6 +69,11 @@ namespace Avalonia.Controls.Utils
public static int PreviousWord(string text, int cursor) public static int PreviousWord(string text, int cursor)
{ {
if (string.IsNullOrEmpty(text))
{
return 0;
}
int begin; int begin;
int i; int i;
int cr; int cr;
@ -107,7 +125,12 @@ namespace Avalonia.Controls.Utils
cr = LineEnd(text, cursor); cr = LineEnd(text, cursor);
if (cr < text.Length && text[cr] == '\r' && text[cr + 1] == '\n') if (cursor >= text.Length)
{
return cursor;
}
if (cr < text.Length && text[cr] == '\r' && cr + 1 < text.Length && text[cr + 1] == '\n')
{ {
lf = cr + 1; lf = cr + 1;
} }

9
src/Avalonia.Controls/ValidatingToplevel.cs

@ -184,7 +184,7 @@ internal class ValidatingWindowBaseImpl : ValidatingToplevelImpl, IWindowBaseImp
public Action? Activated public Action? Activated
{ {
get => Inner.Activated; get => Inner.Activated;
set => Inner.Deactivated = value; set => Inner.Activated = value;
} }
public IPlatformHandle Handle => Inner.Handle; public IPlatformHandle Handle => Inner.Handle;
@ -211,6 +211,13 @@ internal class ValidatingWindowImpl : ValidatingWindowBaseImpl, IWindowImpl
} }
} }
public static IWindowImpl Unwrap(IWindowImpl impl)
{
if (impl is ValidatingWindowImpl v)
return v.Inner;
return impl;
}
public static IWindowImpl Wrap(IWindowImpl impl) public static IWindowImpl Wrap(IWindowImpl impl)
{ {
#if DEBUG #if DEBUG

3
src/Avalonia.DesktopRuntime/ApiCompatBaseline.txt

@ -0,0 +1,3 @@
Compat issues with assembly Avalonia.DesktopRuntime:
TypesMustExist : Type 'Avalonia.Shared.PlatformSupport.AssetLoader' does not exist in the implementation but it does exist in the contract.
Total Issues: 1

2
src/Avalonia.DesktopRuntime/AppBuilder.cs

@ -4,7 +4,7 @@ using System.Linq;
using System.Reflection; using System.Reflection;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Platform; using Avalonia.Platform;
using Avalonia.Shared.PlatformSupport; using Avalonia.PlatformSupport;
namespace Avalonia namespace Avalonia
{ {

2
src/Avalonia.DesktopRuntime/Avalonia.DesktopRuntime.csproj

@ -8,6 +8,7 @@
<ProjectReference Include="../Avalonia.Base/Avalonia.Base.csproj" /> <ProjectReference Include="../Avalonia.Base/Avalonia.Base.csproj" />
<ProjectReference Include="../Avalonia.Visuals/Avalonia.Visuals.csproj" /> <ProjectReference Include="../Avalonia.Visuals/Avalonia.Visuals.csproj" />
<ProjectReference Include="../Avalonia.Controls/Avalonia.Controls.csproj" /> <ProjectReference Include="../Avalonia.Controls/Avalonia.Controls.csproj" />
<ProjectReference Include="../Avalonia.PlatformSupport/Avalonia.PlatformSupport.csproj" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
@ -16,6 +17,5 @@
<Import Project="..\..\build\NetCore.props" /> <Import Project="..\..\build\NetCore.props" />
<Import Project="..\..\build\NetFX.props" /> <Import Project="..\..\build\NetFX.props" />
<Import Project="..\Shared\PlatformSupport\PlatformSupport.projitems" />
<Import Project="..\..\build\ApiDiff.props" /> <Import Project="..\..\build\ApiDiff.props" />
</Project> </Project>

40
src/Avalonia.DesktopRuntime/RuntimeInfo.cs

@ -1,40 +0,0 @@
using System;
using System.Runtime.InteropServices;
using Avalonia.Platform;
namespace Avalonia.Shared.PlatformSupport
{
internal partial class StandardRuntimePlatform
{
private static readonly Lazy<RuntimePlatformInfo> Info = new Lazy<RuntimePlatformInfo>(() =>
{
OperatingSystemType os;
if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
os = OperatingSystemType.OSX;
else if(RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
os = OperatingSystemType.Linux;
else if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
os = OperatingSystemType.WinNT;
else
throw new Exception("Unknown OS platform " + RuntimeInformation.OSDescription);
return new RuntimePlatformInfo
{
#if NETCOREAPP2_0
IsCoreClr = true,
#elif NET461
IsDotNetFramework = false,
#endif
IsDesktop = true,
IsMono = false,
IsMobile = false,
IsUnix = os != OperatingSystemType.WinNT,
OperatingSystem = os,
};
});
public RuntimePlatformInfo GetRuntimeInfo() => Info.Value;
}
}

36
src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs

@ -27,11 +27,6 @@ namespace Avalonia.Headless
public PixelFormat DefaultPixelFormat => PixelFormat.Rgba8888; public PixelFormat DefaultPixelFormat => PixelFormat.Rgba8888;
public IFormattedTextImpl CreateFormattedText(string text, Typeface typeface, double fontSize, TextAlignment textAlignment, TextWrapping wrapping, Size constraint, IReadOnlyList<FormattedTextStyleSpan> spans)
{
return new HeadlessFormattedTextStub(text, constraint);
}
public IGeometryImpl CreateEllipseGeometry(Rect rect) => new HeadlessGeometryStub(rect); public IGeometryImpl CreateEllipseGeometry(Rect rect) => new HeadlessGeometryStub(rect);
public IGeometryImpl CreateLineGeometry(Point p1, Point p2) public IGeometryImpl CreateLineGeometry(Point p1, Point p2)
@ -354,11 +349,6 @@ namespace Avalonia.Headless
} }
public void DrawText(IBrush foreground, Point origin, IFormattedTextImpl text)
{
}
public IDrawingContextLayerImpl CreateLayer(Size size) public IDrawingContextLayerImpl CreateLayer(Size size)
{ {
return new HeadlessBitmapStub(size, new Vector(96, 96)); return new HeadlessBitmapStub(size, new Vector(96, 96));
@ -474,31 +464,5 @@ namespace Avalonia.Headless
return new HeadlessDrawingContextStub(); return new HeadlessDrawingContextStub();
} }
} }
class HeadlessFormattedTextStub : IFormattedTextImpl
{
public HeadlessFormattedTextStub(string text, Size constraint)
{
Text = text;
Constraint = constraint;
Bounds = new Rect(Constraint.Constrain(new Size(50, 50)));
}
public Size Constraint { get; }
public Rect Bounds { get; }
public string Text { get; }
public IEnumerable<FormattedTextLine> GetLines()
{
return new[] { new FormattedTextLine(Text.Length, 10) };
}
public TextHitTestResult HitTestPoint(Point point) => new TextHitTestResult();
public Rect HitTestTextPosition(int index) => new Rect();
public IEnumerable<Rect> HitTestTextRange(int index, int length) => new Rect[length];
}
} }
} }

12
src/Avalonia.Headless/HeadlessPlatformStubs.cs

@ -133,14 +133,10 @@ namespace Avalonia.Headless
class HeadlessTextShaperStub : ITextShaperImpl class HeadlessTextShaperStub : ITextShaperImpl
{ {
public GlyphRun ShapeText(ReadOnlySlice<char> text, Typeface typeface, double fontRenderingEmSize, CultureInfo culture) public ShapedBuffer ShapeText(ReadOnlySlice<char> text, GlyphTypeface typeface, double fontRenderingEmSize,
{ CultureInfo culture, sbyte bidiLevel)
return new GlyphRun(new GlyphTypeface(typeface), 10, {
new ReadOnlySlice<ushort>(new ushort[] { 1, 2, 3 }), return new ShapedBuffer(text, text.Length, typeface, fontRenderingEmSize, bidiLevel);
new ReadOnlySlice<double>(new double[] { 1, 2, 3 }),
new ReadOnlySlice<Vector>(new Vector[] { new Vector(1, 1), new Vector(2, 2), new Vector(3, 3) }),
text,
new ReadOnlySlice<ushort>(new ushort[] { 1, 2, 3 }));
} }
} }

17
src/Shared/PlatformSupport/AssetLoader.cs → src/Avalonia.PlatformSupport/AssetLoader.cs

@ -6,9 +6,7 @@ using System.Reflection;
using Avalonia.Platform; using Avalonia.Platform;
using Avalonia.Utilities; using Avalonia.Utilities;
#nullable enable namespace Avalonia.PlatformSupport
namespace Avalonia.Shared.PlatformSupport
{ {
/// <summary> /// <summary>
/// Loads assets compiled into the application binary. /// Loads assets compiled into the application binary.
@ -233,14 +231,15 @@ namespace Avalonia.Shared.PlatformSupport
else else
{ {
// iOS does not support loading assemblies dynamically! // iOS does not support loading assemblies dynamically!
// #if NET6_0_OR_GREATER
#if __IOS__ if (OperatingSystem.IsIOS())
throw new InvalidOperationException( {
$"Assembly {name} needs to be referenced and explicitly loaded before loading resources"); throw new InvalidOperationException(
#else $"Assembly {name} needs to be referenced and explicitly loaded before loading resources");
}
#endif
name = Uri.UnescapeDataString(name); name = Uri.UnescapeDataString(name);
AssemblyNameCache[name] = rv = new AssemblyDescriptor(Assembly.Load(name)); AssemblyNameCache[name] = rv = new AssemblyDescriptor(Assembly.Load(name));
#endif
} }
} }

18
src/Avalonia.PlatformSupport/Avalonia.PlatformSupport.csproj

@ -0,0 +1,18 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net6.0;net461;netstandard2.0</TargetFrameworks>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="../Avalonia.Base/Avalonia.Base.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="System.Runtime.InteropServices.RuntimeInformation" Version="4.3.0" Condition="'$(TargetFramework)' == 'net461'" />
</ItemGroup>
<Import Project="..\..\build\NetCore.props" />
<Import Project="..\..\build\NetFX.props" />
<Import Project="..\..\build\NullableEnable.props" />
</Project>

65
src/Shared/PlatformSupport/DynLoader.cs → src/Avalonia.PlatformSupport/DynLoader.cs

@ -1,35 +1,40 @@
using System; using System;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using Avalonia.Platform;
using Avalonia.Platform.Interop; using Avalonia.Platform.Interop;
namespace Avalonia.Shared.PlatformSupport // ReSharper disable InconsistentNaming
namespace Avalonia.PlatformSupport
{ {
#if !__IOS__
class UnixLoader : IDynamicLibraryLoader class UnixLoader : IDynamicLibraryLoader
{ {
// ReSharper disable InconsistentNaming
static class LinuxImports static class LinuxImports
{ {
#if __ANDROID__
[DllImport("libdl.so")]
#else
[DllImport("libdl.so.2")] [DllImport("libdl.so.2")]
#endif
private static extern IntPtr dlopen(string path, int flags); private static extern IntPtr dlopen(string path, int flags);
#if __ANDROID__
[DllImport("libdl.so")]
#else
[DllImport("libdl.so.2")] [DllImport("libdl.so.2")]
#endif
private static extern IntPtr dlsym(IntPtr handle, string symbol); private static extern IntPtr dlsym(IntPtr handle, string symbol);
#if __ANDROID__
[DllImport("libdl.so")]
#else
[DllImport("libdl.so.2")] [DllImport("libdl.so.2")]
#endif private static extern IntPtr dlerror();
public static void Init()
{
DlOpen = dlopen;
DlSym = dlsym;
DlError = dlerror;
}
}
static class AndroidImports
{
[DllImport("libdl.so")]
private static extern IntPtr dlopen(string path, int flags);
[DllImport("libdl.so")]
private static extern IntPtr dlsym(IntPtr handle, string symbol);
[DllImport("libdl.so")]
private static extern IntPtr dlerror(); private static extern IntPtr dlerror();
public static void Init() public static void Init()
@ -42,8 +47,6 @@ namespace Avalonia.Shared.PlatformSupport
static class OsXImports static class OsXImports
{ {
[DllImport("/usr/lib/libSystem.dylib")] [DllImport("/usr/lib/libSystem.dylib")]
private static extern IntPtr dlopen(string path, int flags); private static extern IntPtr dlopen(string path, int flags);
@ -72,32 +75,36 @@ namespace Avalonia.Shared.PlatformSupport
uname(buffer); uname(buffer);
var unixName = Marshal.PtrToStringAnsi(buffer); var unixName = Marshal.PtrToStringAnsi(buffer);
Marshal.FreeHGlobal(buffer); Marshal.FreeHGlobal(buffer);
if(unixName == "Darwin") if (unixName == "Darwin")
OsXImports.Init(); OsXImports.Init();
#if NET6_0_OR_GREATER
else if (OperatingSystem.IsAndroid())
AndroidImports.Init();
#endif
else else
LinuxImports.Init(); LinuxImports.Init();
} }
private static Func<string, int, IntPtr> DlOpen; private static Func<string, int, IntPtr>? DlOpen;
private static Func<IntPtr, string, IntPtr> DlSym; private static Func<IntPtr, string, IntPtr>? DlSym;
private static Func<IntPtr> DlError; private static Func<IntPtr>? DlError;
// ReSharper restore InconsistentNaming // ReSharper restore InconsistentNaming
static string DlErrorString() => Marshal.PtrToStringAnsi(DlError()); static string? DlErrorString() => Marshal.PtrToStringAnsi(DlError!.Invoke());
public IntPtr LoadLibrary(string dll) public IntPtr LoadLibrary(string dll)
{ {
var handle = DlOpen(dll, 1); var handle = DlOpen!.Invoke(dll, 1);
if (handle == IntPtr.Zero) if (handle == IntPtr.Zero)
throw new DynamicLibraryLoaderException(DlErrorString()); throw new DynamicLibraryLoaderException(DlErrorString()!);
return handle; return handle;
} }
public IntPtr GetProcAddress(IntPtr dll, string proc, bool optional) public IntPtr GetProcAddress(IntPtr dll, string proc, bool optional)
{ {
var ptr = DlSym(dll, proc); var ptr = DlSym!.Invoke(dll, proc);
if (ptr == IntPtr.Zero && !optional) if (ptr == IntPtr.Zero && !optional)
throw new DynamicLibraryLoaderException(DlErrorString()); throw new DynamicLibraryLoaderException(DlErrorString()!);
return ptr; return ptr;
} }
} }
@ -129,8 +136,7 @@ namespace Avalonia.Shared.PlatformSupport
} }
} }
#else internal class NotSupportedLoader : IDynamicLibraryLoader
internal class IOSLoader : IDynamicLibraryLoader
{ {
IntPtr IDynamicLibraryLoader.LoadLibrary(string dll) IntPtr IDynamicLibraryLoader.LoadLibrary(string dll)
{ {
@ -142,5 +148,4 @@ namespace Avalonia.Shared.PlatformSupport
throw new PlatformNotSupportedException(); throw new PlatformNotSupportedException();
} }
} }
#endif
} }

79
src/Shared/PlatformSupport/StandardRuntimePlatform.cs → src/Avalonia.PlatformSupport/StandardRuntimePlatform.cs

@ -1,14 +1,13 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Reflection;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Threading; using System.Threading;
using Avalonia.Platform; using Avalonia.Platform;
namespace Avalonia.Shared.PlatformSupport namespace Avalonia.PlatformSupport
{ {
internal partial class StandardRuntimePlatform : IRuntimePlatform public class StandardRuntimePlatform : IRuntimePlatform
{ {
public IDisposable StartSystemTimer(TimeSpan interval, Action tick) public IDisposable StartSystemTimer(TimeSpan interval, Action tick)
{ {
@ -16,15 +15,15 @@ namespace Avalonia.Shared.PlatformSupport
} }
public IUnmanagedBlob AllocBlob(int size) => new UnmanagedBlob(this, size); public IUnmanagedBlob AllocBlob(int size) => new UnmanagedBlob(this, size);
class UnmanagedBlob : IUnmanagedBlob private class UnmanagedBlob : IUnmanagedBlob
{ {
private readonly StandardRuntimePlatform _plat; private readonly StandardRuntimePlatform _plat;
private IntPtr _address; private IntPtr _address;
private readonly object _lock = new object(); private readonly object _lock = new object();
#if DEBUG #if DEBUG
private static readonly List<string> Backtraces = new List<string>(); private static readonly List<string> Backtraces = new List<string>();
private static Thread GCThread; private static Thread? GCThread;
private readonly string _backtrace; private readonly string _backtrace;
private static readonly object _btlock = new object(); private static readonly object _btlock = new object();
@ -38,15 +37,14 @@ namespace Avalonia.Shared.PlatformSupport
[MethodImpl(MethodImplOptions.NoInlining)] [MethodImpl(MethodImplOptions.NoInlining)]
static void Spawn() => new GCThreadDetector(); static void Spawn() => new GCThreadDetector();
static UnmanagedBlob() static UnmanagedBlob()
{ {
Spawn(); Spawn();
GC.WaitForPendingFinalizers(); GC.WaitForPendingFinalizers();
} }
#endif #endif
public UnmanagedBlob(StandardRuntimePlatform plat, int size) public UnmanagedBlob(StandardRuntimePlatform plat, int size)
{ {
try try
@ -117,14 +115,12 @@ namespace Avalonia.Shared.PlatformSupport
DoDispose(); DoDispose();
} }
public IntPtr Address => IsDisposed ? throw new ObjectDisposedException("UnmanagedBlob") : _address; public IntPtr Address => IsDisposed ? throw new ObjectDisposedException("UnmanagedBlob") : _address;
public int Size { get; private set; } public int Size { get; private set; }
public bool IsDisposed { get; private set; } public bool IsDisposed { get; private set; }
} }
#if NET461 || NETCOREAPP2_0_OR_GREATER
#if NET461 || NETCOREAPP2_0
[DllImport("libc", SetLastError = true)] [DllImport("libc", SetLastError = true)]
private static extern IntPtr mmap(IntPtr addr, IntPtr length, int prot, int flags, int fd, IntPtr offset); private static extern IntPtr mmap(IntPtr addr, IntPtr length, int prot, int flags, int fd, IntPtr offset);
[DllImport("libc", SetLastError = true)] [DllImport("libc", SetLastError = true)]
@ -133,15 +129,15 @@ namespace Avalonia.Shared.PlatformSupport
private static extern long sysconf(int name); private static extern long sysconf(int name);
private bool? _useMmap; private bool? _useMmap;
private bool UseMmap private bool UseMmap
=> _useMmap ?? ((_useMmap = GetRuntimeInfo().OperatingSystem == OperatingSystemType.Linux)).Value; => _useMmap ?? ((_useMmap = GetRuntimeInfo().OperatingSystem == OperatingSystemType.Linux)).Value;
IntPtr Alloc(int size) IntPtr Alloc(int size)
{ {
if (UseMmap) if (UseMmap)
{ {
var rv = mmap(IntPtr.Zero, new IntPtr(size), 3, 0x22, -1, IntPtr.Zero); var rv = mmap(IntPtr.Zero, new IntPtr(size), 3, 0x22, -1, IntPtr.Zero);
if (rv.ToInt64() == -1 || (ulong) rv.ToInt64() == 0xffffffff) if (rv.ToInt64() == -1 || (ulong)rv.ToInt64() == 0xffffffff)
{ {
var errno = Marshal.GetLastWin32Error(); var errno = Marshal.GetLastWin32Error();
throw new Exception("Unable to allocate memory: " + errno); throw new Exception("Unable to allocate memory: " + errno);
@ -169,5 +165,54 @@ namespace Avalonia.Shared.PlatformSupport
IntPtr Alloc(int size) => Marshal.AllocHGlobal(size); IntPtr Alloc(int size) => Marshal.AllocHGlobal(size);
void Free(IntPtr ptr, int len) => Marshal.FreeHGlobal(ptr); void Free(IntPtr ptr, int len) => Marshal.FreeHGlobal(ptr);
#endif #endif
private static readonly Lazy<RuntimePlatformInfo> Info = new Lazy<RuntimePlatformInfo>(() =>
{
OperatingSystemType os;
#if NET5_0_OR_GREATER
if (OperatingSystem.IsWindows())
os = OperatingSystemType.WinNT;
else if (OperatingSystem.IsMacOS())
os = OperatingSystemType.OSX;
else if (OperatingSystem.IsLinux() || OperatingSystem.IsFreeBSD())
os = OperatingSystemType.Linux;
else if (OperatingSystem.IsAndroid())
os = OperatingSystemType.Android;
else if (OperatingSystem.IsIOS())
os = OperatingSystemType.iOS;
else if (OperatingSystem.IsBrowser())
os = OperatingSystemType.Browser;
else
throw new Exception("Unknown OS platform " + RuntimeInformation.OSDescription);
#else
if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
os = OperatingSystemType.OSX;
else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
os = OperatingSystemType.Linux;
else if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
os = OperatingSystemType.WinNT;
else
throw new Exception("Unknown OS platform " + RuntimeInformation.OSDescription);
#endif
return new RuntimePlatformInfo
{
#if NETCOREAPP
IsCoreClr = true,
#elif NETFRAMEWORK
IsDotNetFramework = false,
#endif
IsDesktop = os == OperatingSystemType.Linux || os == OperatingSystemType.OSX || os == OperatingSystemType.WinNT,
IsMono = os == OperatingSystemType.Android || os == OperatingSystemType.iOS || os == OperatingSystemType.Browser,
IsMobile = os == OperatingSystemType.Android || os == OperatingSystemType.iOS,
IsUnix = os == OperatingSystemType.Linux || os == OperatingSystemType.OSX || os == OperatingSystemType.Android,
IsBrowser = os == OperatingSystemType.Browser,
OperatingSystem = os,
};
});
public virtual RuntimePlatformInfo GetRuntimeInfo() => Info.Value;
} }
} }

31
src/Avalonia.PlatformSupport/StandardRuntimePlatformServices.cs

@ -0,0 +1,31 @@
using System.Reflection;
using Avalonia.Platform;
using Avalonia.Platform.Interop;
namespace Avalonia.PlatformSupport
{
public static class StandardRuntimePlatformServices
{
public static void Register(Assembly? assembly = null)
{
var standardPlatform = new StandardRuntimePlatform();
var os = standardPlatform.GetRuntimeInfo().OperatingSystem;
AssetLoader.RegisterResUriParsers();
AvaloniaLocator.CurrentMutable
.Bind<IRuntimePlatform>().ToConstant(standardPlatform)
.Bind<IAssetLoader>().ToConstant(new AssetLoader(assembly))
.Bind<IDynamicLibraryLoader>().ToConstant(
os switch
{
OperatingSystemType.WinNT => new Win32Loader(),
OperatingSystemType.OSX => new UnixLoader(),
OperatingSystemType.Linux => new UnixLoader(),
OperatingSystemType.Android => new UnixLoader(),
// iOS, WASM, ...
_ => (IDynamicLibraryLoader)new NotSupportedLoader()
}
);
}
}
}

61
src/Avalonia.Visuals/ApiCompatBaseline.txt

@ -5,11 +5,50 @@ InterfacesShouldHaveSameMembers : Interface member 'public System.Threading.Task
MembersMustExist : Member 'public System.Threading.Tasks.Task Avalonia.Animation.IPageTransition.Start(Avalonia.Visual, Avalonia.Visual, System.Boolean)' does not exist in the implementation but it does exist in the contract. MembersMustExist : Member 'public System.Threading.Tasks.Task Avalonia.Animation.IPageTransition.Start(Avalonia.Visual, Avalonia.Visual, System.Boolean)' does not exist in the implementation but it does exist in the contract.
InterfacesShouldHaveSameMembers : Interface member 'public System.Threading.Tasks.Task Avalonia.Animation.IPageTransition.Start(Avalonia.Visual, Avalonia.Visual, System.Boolean, System.Threading.CancellationToken)' is present in the implementation but not in the contract. InterfacesShouldHaveSameMembers : Interface member 'public System.Threading.Tasks.Task Avalonia.Animation.IPageTransition.Start(Avalonia.Visual, Avalonia.Visual, System.Boolean, System.Threading.CancellationToken)' is present in the implementation but not in the contract.
MembersMustExist : Member 'public System.Threading.Tasks.Task Avalonia.Animation.PageSlide.Start(Avalonia.Visual, Avalonia.Visual, System.Boolean)' does not exist in the implementation but it does exist in the contract. MembersMustExist : Member 'public System.Threading.Tasks.Task Avalonia.Animation.PageSlide.Start(Avalonia.Visual, Avalonia.Visual, System.Boolean)' does not exist in the implementation but it does exist in the contract.
MembersMustExist : Member 'public void Avalonia.Media.DrawingContext.DrawText(Avalonia.Media.IBrush, Avalonia.Point, Avalonia.Media.FormattedText)' does not exist in the implementation but it does exist in the contract.
MembersMustExist : Member 'public void Avalonia.Media.FormattedText..ctor()' does not exist in the implementation but it does exist in the contract.
MembersMustExist : Member 'public void Avalonia.Media.FormattedText..ctor(Avalonia.Platform.IPlatformRenderInterface)' does not exist in the implementation but it does exist in the contract.
MembersMustExist : Member 'public void Avalonia.Media.FormattedText..ctor(System.String, Avalonia.Media.Typeface, System.Double, Avalonia.Media.TextAlignment, Avalonia.Media.TextWrapping, Avalonia.Size)' does not exist in the implementation but it does exist in the contract.
MembersMustExist : Member 'public Avalonia.Rect Avalonia.Media.FormattedText.Bounds.get()' does not exist in the implementation but it does exist in the contract.
MembersMustExist : Member 'public Avalonia.Size Avalonia.Media.FormattedText.Constraint.get()' does not exist in the implementation but it does exist in the contract.
MembersMustExist : Member 'public void Avalonia.Media.FormattedText.Constraint.set(Avalonia.Size)' does not exist in the implementation but it does exist in the contract.
MembersMustExist : Member 'public System.Double Avalonia.Media.FormattedText.FontSize.get()' does not exist in the implementation but it does exist in the contract.
MembersMustExist : Member 'public void Avalonia.Media.FormattedText.FontSize.set(System.Double)' does not exist in the implementation but it does exist in the contract.
MembersMustExist : Member 'public System.Collections.Generic.IEnumerable<Avalonia.Media.FormattedTextLine> Avalonia.Media.FormattedText.GetLines()' does not exist in the implementation but it does exist in the contract.
MembersMustExist : Member 'public Avalonia.Media.TextHitTestResult Avalonia.Media.FormattedText.HitTestPoint(Avalonia.Point)' does not exist in the implementation but it does exist in the contract.
MembersMustExist : Member 'public Avalonia.Rect Avalonia.Media.FormattedText.HitTestTextPosition(System.Int32)' does not exist in the implementation but it does exist in the contract.
MembersMustExist : Member 'public System.Collections.Generic.IEnumerable<Avalonia.Rect> Avalonia.Media.FormattedText.HitTestTextRange(System.Int32, System.Int32)' does not exist in the implementation but it does exist in the contract.
MembersMustExist : Member 'public Avalonia.Platform.IFormattedTextImpl Avalonia.Media.FormattedText.PlatformImpl.get()' does not exist in the implementation but it does exist in the contract.
MembersMustExist : Member 'public System.Collections.Generic.IReadOnlyList<Avalonia.Media.FormattedTextStyleSpan> Avalonia.Media.FormattedText.Spans.get()' does not exist in the implementation but it does exist in the contract.
MembersMustExist : Member 'public void Avalonia.Media.FormattedText.Spans.set(System.Collections.Generic.IReadOnlyList<Avalonia.Media.FormattedTextStyleSpan>)' does not exist in the implementation but it does exist in the contract.
MembersMustExist : Member 'public System.String Avalonia.Media.FormattedText.Text.get()' does not exist in the implementation but it does exist in the contract.
MembersMustExist : Member 'public void Avalonia.Media.FormattedText.Text.set(System.String)' does not exist in the implementation but it does exist in the contract.
MembersMustExist : Member 'public Avalonia.Media.TextWrapping Avalonia.Media.FormattedText.TextWrapping.get()' does not exist in the implementation but it does exist in the contract.
MembersMustExist : Member 'public void Avalonia.Media.FormattedText.TextWrapping.set(Avalonia.Media.TextWrapping)' does not exist in the implementation but it does exist in the contract.
MembersMustExist : Member 'public Avalonia.Media.Typeface Avalonia.Media.FormattedText.Typeface.get()' does not exist in the implementation but it does exist in the contract.
MembersMustExist : Member 'public void Avalonia.Media.FormattedText.Typeface.set(Avalonia.Media.Typeface)' does not exist in the implementation but it does exist in the contract.
TypesMustExist : Type 'Avalonia.Media.FormattedTextLine' does not exist in the implementation but it does exist in the contract.
TypesMustExist : Type 'Avalonia.Media.FormattedTextStyleSpan' does not exist in the implementation but it does exist in the contract.
MembersMustExist : Member 'public void Avalonia.Media.GlyphRun..ctor()' does not exist in the implementation but it does exist in the contract. MembersMustExist : Member 'public void Avalonia.Media.GlyphRun..ctor()' does not exist in the implementation but it does exist in the contract.
MembersMustExist : Member 'public void Avalonia.Media.GlyphRun..ctor(Avalonia.Media.GlyphTypeface, System.Double, Avalonia.Utilities.ReadOnlySlice<System.UInt16>, Avalonia.Utilities.ReadOnlySlice<System.Double>, Avalonia.Utilities.ReadOnlySlice<Avalonia.Vector>, Avalonia.Utilities.ReadOnlySlice<System.Char>, Avalonia.Utilities.ReadOnlySlice<System.UInt16>, System.Int32)' does not exist in the implementation but it does exist in the contract.
MembersMustExist : Member 'public Avalonia.Utilities.ReadOnlySlice<System.Double> Avalonia.Media.GlyphRun.GlyphAdvances.get()' does not exist in the implementation but it does exist in the contract.
MembersMustExist : Member 'public void Avalonia.Media.GlyphRun.GlyphAdvances.set(Avalonia.Utilities.ReadOnlySlice<System.Double>)' does not exist in the implementation but it does exist in the contract.
MembersMustExist : Member 'public Avalonia.Utilities.ReadOnlySlice<System.UInt16> Avalonia.Media.GlyphRun.GlyphClusters.get()' does not exist in the implementation but it does exist in the contract.
MembersMustExist : Member 'public void Avalonia.Media.GlyphRun.GlyphClusters.set(Avalonia.Utilities.ReadOnlySlice<System.UInt16>)' does not exist in the implementation but it does exist in the contract.
MembersMustExist : Member 'public Avalonia.Utilities.ReadOnlySlice<System.UInt16> Avalonia.Media.GlyphRun.GlyphIndices.get()' does not exist in the implementation but it does exist in the contract.
MembersMustExist : Member 'public void Avalonia.Media.GlyphRun.GlyphIndices.set(Avalonia.Utilities.ReadOnlySlice<System.UInt16>)' does not exist in the implementation but it does exist in the contract.
MembersMustExist : Member 'public Avalonia.Utilities.ReadOnlySlice<Avalonia.Vector> Avalonia.Media.GlyphRun.GlyphOffsets.get()' does not exist in the implementation but it does exist in the contract.
MembersMustExist : Member 'public void Avalonia.Media.GlyphRun.GlyphOffsets.set(Avalonia.Utilities.ReadOnlySlice<Avalonia.Vector>)' does not exist in the implementation but it does exist in the contract.
MembersMustExist : Member 'public void Avalonia.Media.GlyphRun.GlyphTypeface.set(Avalonia.Media.GlyphTypeface)' does not exist in the implementation but it does exist in the contract. MembersMustExist : Member 'public void Avalonia.Media.GlyphRun.GlyphTypeface.set(Avalonia.Media.GlyphTypeface)' does not exist in the implementation but it does exist in the contract.
CannotSealType : Type 'Avalonia.Media.Pen' is actually (has the sealed modifier) sealed in the implementation but not sealed in the contract. CannotSealType : Type 'Avalonia.Media.Pen' is actually (has the sealed modifier) sealed in the implementation but not sealed in the contract.
MembersMustExist : Member 'protected void Avalonia.Media.Pen.AffectsRender<T>(Avalonia.AvaloniaProperty[])' does not exist in the implementation but it does exist in the contract. MembersMustExist : Member 'protected void Avalonia.Media.Pen.AffectsRender<T>(Avalonia.AvaloniaProperty[])' does not exist in the implementation but it does exist in the contract.
MembersMustExist : Member 'protected void Avalonia.Media.Pen.RaiseInvalidated(System.EventArgs)' does not exist in the implementation but it does exist in the contract. MembersMustExist : Member 'protected void Avalonia.Media.Pen.RaiseInvalidated(System.EventArgs)' does not exist in the implementation but it does exist in the contract.
CannotSealType : Type 'Avalonia.Media.TextHitTestResult' is actually (has the sealed modifier) sealed in the implementation but not sealed in the contract.
TypeCannotChangeClassification : Type 'Avalonia.Media.TextHitTestResult' is a 'struct' in the implementation but is a 'class' in the contract.
MembersMustExist : Member 'public void Avalonia.Media.TextHitTestResult..ctor()' does not exist in the implementation but it does exist in the contract.
MembersMustExist : Member 'public void Avalonia.Media.TextHitTestResult.IsInside.set(System.Boolean)' does not exist in the implementation but it does exist in the contract.
MembersMustExist : Member 'public void Avalonia.Media.TextHitTestResult.IsTrailing.set(System.Boolean)' does not exist in the implementation but it does exist in the contract.
MembersMustExist : Member 'public void Avalonia.Media.TextHitTestResult.TextPosition.set(System.Int32)' does not exist in the implementation but it does exist in the contract.
TypeCannotChangeClassification : Type 'Avalonia.Media.Immutable.ImmutableSolidColorBrush' is a 'class' in the implementation but is a 'struct' in the contract. TypeCannotChangeClassification : Type 'Avalonia.Media.Immutable.ImmutableSolidColorBrush' is a 'class' in the implementation but is a 'struct' in the contract.
MembersMustExist : Member 'public void Avalonia.Media.TextFormatting.DrawableTextRun.Draw(Avalonia.Media.DrawingContext)' does not exist in the implementation but it does exist in the contract. MembersMustExist : Member 'public void Avalonia.Media.TextFormatting.DrawableTextRun.Draw(Avalonia.Media.DrawingContext)' does not exist in the implementation but it does exist in the contract.
CannotAddAbstractMembers : Member 'public void Avalonia.Media.TextFormatting.DrawableTextRun.Draw(Avalonia.Media.DrawingContext, Avalonia.Point)' is abstract in the implementation but is missing in the contract. CannotAddAbstractMembers : Member 'public void Avalonia.Media.TextFormatting.DrawableTextRun.Draw(Avalonia.Media.DrawingContext, Avalonia.Point)' is abstract in the implementation but is missing in the contract.
@ -23,8 +62,14 @@ CannotMakeMemberNonVirtual : Member 'public System.Double Avalonia.Media.TextFor
CannotMakeMemberNonVirtual : Member 'public Avalonia.Media.TextAlignment Avalonia.Media.TextFormatting.GenericTextParagraphProperties.TextAlignment.get()' is non-virtual in the implementation but is virtual in the contract. CannotMakeMemberNonVirtual : Member 'public Avalonia.Media.TextAlignment Avalonia.Media.TextFormatting.GenericTextParagraphProperties.TextAlignment.get()' is non-virtual in the implementation but is virtual in the contract.
CannotMakeMemberNonVirtual : Member 'public Avalonia.Media.TextWrapping Avalonia.Media.TextFormatting.GenericTextParagraphProperties.TextWrapping.get()' is non-virtual in the implementation but is virtual in the contract. CannotMakeMemberNonVirtual : Member 'public Avalonia.Media.TextWrapping Avalonia.Media.TextFormatting.GenericTextParagraphProperties.TextWrapping.get()' is non-virtual in the implementation but is virtual in the contract.
MembersMustExist : Member 'public void Avalonia.Media.TextFormatting.GenericTextRunProperties..ctor(Avalonia.Media.Typeface, System.Double, Avalonia.Media.TextDecorationCollection, Avalonia.Media.IBrush, Avalonia.Media.IBrush, System.Globalization.CultureInfo)' does not exist in the implementation but it does exist in the contract. MembersMustExist : Member 'public void Avalonia.Media.TextFormatting.GenericTextRunProperties..ctor(Avalonia.Media.Typeface, System.Double, Avalonia.Media.TextDecorationCollection, Avalonia.Media.IBrush, Avalonia.Media.IBrush, System.Globalization.CultureInfo)' does not exist in the implementation but it does exist in the contract.
MembersMustExist : Member 'public void Avalonia.Media.TextFormatting.ShapeableTextCharacters..ctor(Avalonia.Utilities.ReadOnlySlice<System.Char>, Avalonia.Media.TextFormatting.TextRunProperties)' does not exist in the implementation but it does exist in the contract.
MembersMustExist : Member 'public void Avalonia.Media.TextFormatting.ShapedTextCharacters..ctor(Avalonia.Media.GlyphRun, Avalonia.Media.TextFormatting.TextRunProperties)' does not exist in the implementation but it does exist in the contract.
MembersMustExist : Member 'public void Avalonia.Media.TextFormatting.ShapedTextCharacters.Draw(Avalonia.Media.DrawingContext)' does not exist in the implementation but it does exist in the contract. MembersMustExist : Member 'public void Avalonia.Media.TextFormatting.ShapedTextCharacters.Draw(Avalonia.Media.DrawingContext)' does not exist in the implementation but it does exist in the contract.
MembersMustExist : Member 'public Avalonia.Media.TextFormatting.ShapedTextCharacters.SplitTextCharactersResult Avalonia.Media.TextFormatting.ShapedTextCharacters.Split(System.Int32)' does not exist in the implementation but it does exist in the contract.
TypesMustExist : Type 'Avalonia.Media.TextFormatting.ShapedTextCharacters.SplitTextCharactersResult' does not exist in the implementation but it does exist in the contract.
MembersMustExist : Member 'protected System.Boolean Avalonia.Media.TextFormatting.TextCharacters.TryGetRunProperties(Avalonia.Utilities.ReadOnlySlice<System.Char>, Avalonia.Media.Typeface, Avalonia.Media.Typeface, System.Int32)' does not exist in the implementation but it does exist in the contract.
MembersMustExist : Member 'public void Avalonia.Media.TextFormatting.TextEndOfLine..ctor()' does not exist in the implementation but it does exist in the contract. MembersMustExist : Member 'public void Avalonia.Media.TextFormatting.TextEndOfLine..ctor()' does not exist in the implementation but it does exist in the contract.
MembersMustExist : Member 'public void Avalonia.Media.TextFormatting.TextLayout..ctor(System.String, Avalonia.Media.Typeface, System.Double, Avalonia.Media.IBrush, Avalonia.Media.TextAlignment, Avalonia.Media.TextWrapping, Avalonia.Media.TextTrimming, Avalonia.Media.TextDecorationCollection, System.Double, System.Double, System.Double, System.Int32, System.Collections.Generic.IReadOnlyList<Avalonia.Utilities.ValueSpan<Avalonia.Media.TextFormatting.TextRunProperties>>)' does not exist in the implementation but it does exist in the contract.
MembersMustExist : Member 'public void Avalonia.Media.TextFormatting.TextLayout.Draw(Avalonia.Media.DrawingContext)' does not exist in the implementation but it does exist in the contract. MembersMustExist : Member 'public void Avalonia.Media.TextFormatting.TextLayout.Draw(Avalonia.Media.DrawingContext)' does not exist in the implementation but it does exist in the contract.
CannotAddAbstractMembers : Member 'public System.Double Avalonia.Media.TextFormatting.TextLine.Baseline' is abstract in the implementation but is missing in the contract. CannotAddAbstractMembers : Member 'public System.Double Avalonia.Media.TextFormatting.TextLine.Baseline' is abstract in the implementation but is missing in the contract.
CannotAddAbstractMembers : Member 'public System.Double Avalonia.Media.TextFormatting.TextLine.Extent' is abstract in the implementation but is missing in the contract. CannotAddAbstractMembers : Member 'public System.Double Avalonia.Media.TextFormatting.TextLine.Extent' is abstract in the implementation but is missing in the contract.
@ -53,6 +98,7 @@ CannotAddAbstractMembers : Member 'public System.Double Avalonia.Media.TextForma
CannotAddAbstractMembers : Member 'public System.Int32 Avalonia.Media.TextFormatting.TextLine.TrailingWhitespaceLength.get()' is abstract in the implementation but is missing in the contract. CannotAddAbstractMembers : Member 'public System.Int32 Avalonia.Media.TextFormatting.TextLine.TrailingWhitespaceLength.get()' is abstract in the implementation but is missing in the contract.
CannotAddAbstractMembers : Member 'public System.Double Avalonia.Media.TextFormatting.TextLine.Width.get()' is abstract in the implementation but is missing in the contract. CannotAddAbstractMembers : Member 'public System.Double Avalonia.Media.TextFormatting.TextLine.Width.get()' is abstract in the implementation but is missing in the contract.
CannotAddAbstractMembers : Member 'public System.Double Avalonia.Media.TextFormatting.TextLine.WidthIncludingTrailingWhitespace.get()' is abstract in the implementation but is missing in the contract. CannotAddAbstractMembers : Member 'public System.Double Avalonia.Media.TextFormatting.TextLine.WidthIncludingTrailingWhitespace.get()' is abstract in the implementation but is missing in the contract.
MembersMustExist : Member 'public void Avalonia.Media.TextFormatting.TextLineBreak..ctor(System.Collections.Generic.IReadOnlyList<Avalonia.Media.TextFormatting.ShapedTextCharacters>)' does not exist in the implementation but it does exist in the contract.
MembersMustExist : Member 'public void Avalonia.Media.TextFormatting.TextLineMetrics..ctor(Avalonia.Size, System.Double, Avalonia.Media.TextFormatting.TextRange, System.Boolean)' does not exist in the implementation but it does exist in the contract. MembersMustExist : Member 'public void Avalonia.Media.TextFormatting.TextLineMetrics..ctor(Avalonia.Size, System.Double, Avalonia.Media.TextFormatting.TextRange, System.Boolean)' does not exist in the implementation but it does exist in the contract.
MembersMustExist : Member 'public Avalonia.Media.TextFormatting.TextLineMetrics Avalonia.Media.TextFormatting.TextLineMetrics.Create(System.Collections.Generic.IEnumerable<Avalonia.Media.TextFormatting.TextRun>, Avalonia.Media.TextFormatting.TextRange, System.Double, Avalonia.Media.TextFormatting.TextParagraphProperties)' does not exist in the implementation but it does exist in the contract. MembersMustExist : Member 'public Avalonia.Media.TextFormatting.TextLineMetrics Avalonia.Media.TextFormatting.TextLineMetrics.Create(System.Collections.Generic.IEnumerable<Avalonia.Media.TextFormatting.TextRun>, Avalonia.Media.TextFormatting.TextRange, System.Double, Avalonia.Media.TextFormatting.TextParagraphProperties)' does not exist in the implementation but it does exist in the contract.
MembersMustExist : Member 'public Avalonia.Size Avalonia.Media.TextFormatting.TextLineMetrics.Size.get()' does not exist in the implementation but it does exist in the contract. MembersMustExist : Member 'public Avalonia.Size Avalonia.Media.TextFormatting.TextLineMetrics.Size.get()' does not exist in the implementation but it does exist in the contract.
@ -65,15 +111,23 @@ CannotAddAbstractMembers : Member 'public Avalonia.Media.FlowDirection Avalonia.
CannotAddAbstractMembers : Member 'public System.Double Avalonia.Media.TextFormatting.TextParagraphProperties.Indent.get()' is abstract in the implementation but is missing in the contract. CannotAddAbstractMembers : Member 'public System.Double Avalonia.Media.TextFormatting.TextParagraphProperties.Indent.get()' is abstract in the implementation but is missing in the contract.
CannotAddAbstractMembers : Member 'public Avalonia.Media.BaselineAlignment Avalonia.Media.TextFormatting.TextRunProperties.BaselineAlignment' is abstract in the implementation but is missing in the contract. CannotAddAbstractMembers : Member 'public Avalonia.Media.BaselineAlignment Avalonia.Media.TextFormatting.TextRunProperties.BaselineAlignment' is abstract in the implementation but is missing in the contract.
CannotAddAbstractMembers : Member 'public Avalonia.Media.BaselineAlignment Avalonia.Media.TextFormatting.TextRunProperties.BaselineAlignment.get()' is abstract in the implementation but is missing in the contract. CannotAddAbstractMembers : Member 'public Avalonia.Media.BaselineAlignment Avalonia.Media.TextFormatting.TextRunProperties.BaselineAlignment.get()' is abstract in the implementation but is missing in the contract.
MembersMustExist : Member 'public Avalonia.Media.GlyphRun Avalonia.Media.TextFormatting.TextShaper.ShapeText(Avalonia.Utilities.ReadOnlySlice<System.Char>, Avalonia.Media.Typeface, System.Double, System.Globalization.CultureInfo)' does not exist in the implementation but it does exist in the contract.
TypesMustExist : Type 'Avalonia.Media.TextFormatting.Unicode.BiDiClass' does not exist in the implementation but it does exist in the contract.
MembersMustExist : Member 'public Avalonia.Media.TextFormatting.Unicode.BiDiClass Avalonia.Media.TextFormatting.Unicode.Codepoint.BiDiClass.get()' does not exist in the implementation but it does exist in the contract.
InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Platform.IDrawingContextImpl.DrawEllipse(Avalonia.Media.IBrush, Avalonia.Media.IPen, Avalonia.Rect)' is present in the implementation but not in the contract. InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Platform.IDrawingContextImpl.DrawEllipse(Avalonia.Media.IBrush, Avalonia.Media.IPen, Avalonia.Rect)' is present in the implementation but not in the contract.
InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Platform.IDrawingContextImpl.DrawText(Avalonia.Media.IBrush, Avalonia.Point, Avalonia.Platform.IFormattedTextImpl)' is present in the contract but not in the implementation.
MembersMustExist : Member 'public void Avalonia.Platform.IDrawingContextImpl.DrawText(Avalonia.Media.IBrush, Avalonia.Point, Avalonia.Platform.IFormattedTextImpl)' does not exist in the implementation but it does exist in the contract.
InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Platform.IDrawingContextImpl.PopBitmapBlendMode()' is present in the implementation but not in the contract. InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Platform.IDrawingContextImpl.PopBitmapBlendMode()' is present in the implementation but not in the contract.
InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Platform.IDrawingContextImpl.PushBitmapBlendMode(Avalonia.Visuals.Media.Imaging.BitmapBlendingMode)' is present in the implementation but not in the contract. InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Platform.IDrawingContextImpl.PushBitmapBlendMode(Avalonia.Visuals.Media.Imaging.BitmapBlendingMode)' is present in the implementation but not in the contract.
TypesMustExist : Type 'Avalonia.Platform.IFormattedTextImpl' does not exist in the implementation but it does exist in the contract.
InterfacesShouldHaveSameMembers : Interface member 'public System.Double Avalonia.Platform.IGeometryImpl.ContourLength' is present in the implementation but not in the contract. InterfacesShouldHaveSameMembers : Interface member 'public System.Double Avalonia.Platform.IGeometryImpl.ContourLength' is present in the implementation but not in the contract.
InterfacesShouldHaveSameMembers : Interface member 'public System.Double Avalonia.Platform.IGeometryImpl.ContourLength.get()' is present in the implementation but not in the contract. InterfacesShouldHaveSameMembers : Interface member 'public System.Double Avalonia.Platform.IGeometryImpl.ContourLength.get()' is present in the implementation but not in the contract.
InterfacesShouldHaveSameMembers : Interface member 'public System.Boolean Avalonia.Platform.IGeometryImpl.TryGetPointAndTangentAtDistance(System.Double, Avalonia.Point, Avalonia.Point)' is present in the implementation but not in the contract. InterfacesShouldHaveSameMembers : Interface member 'public System.Boolean Avalonia.Platform.IGeometryImpl.TryGetPointAndTangentAtDistance(System.Double, Avalonia.Point, Avalonia.Point)' is present in the implementation but not in the contract.
InterfacesShouldHaveSameMembers : Interface member 'public System.Boolean Avalonia.Platform.IGeometryImpl.TryGetPointAtDistance(System.Double, Avalonia.Point)' is present in the implementation but not in the contract. InterfacesShouldHaveSameMembers : Interface member 'public System.Boolean Avalonia.Platform.IGeometryImpl.TryGetPointAtDistance(System.Double, Avalonia.Point)' is present in the implementation but not in the contract.
InterfacesShouldHaveSameMembers : Interface member 'public System.Boolean Avalonia.Platform.IGeometryImpl.TryGetSegment(System.Double, System.Double, System.Boolean, Avalonia.Platform.IGeometryImpl)' is present in the implementation but not in the contract. InterfacesShouldHaveSameMembers : Interface member 'public System.Boolean Avalonia.Platform.IGeometryImpl.TryGetSegment(System.Double, System.Double, System.Boolean, Avalonia.Platform.IGeometryImpl)' is present in the implementation but not in the contract.
InterfacesShouldHaveSameMembers : Interface member 'public Avalonia.Platform.IGeometryImpl Avalonia.Platform.IPlatformRenderInterface.CreateCombinedGeometry(Avalonia.Media.GeometryCombineMode, Avalonia.Media.Geometry, Avalonia.Media.Geometry)' is present in the implementation but not in the contract. InterfacesShouldHaveSameMembers : Interface member 'public Avalonia.Platform.IGeometryImpl Avalonia.Platform.IPlatformRenderInterface.CreateCombinedGeometry(Avalonia.Media.GeometryCombineMode, Avalonia.Media.Geometry, Avalonia.Media.Geometry)' is present in the implementation but not in the contract.
InterfacesShouldHaveSameMembers : Interface member 'public Avalonia.Platform.IFormattedTextImpl Avalonia.Platform.IPlatformRenderInterface.CreateFormattedText(System.String, Avalonia.Media.Typeface, System.Double, Avalonia.Media.TextAlignment, Avalonia.Media.TextWrapping, Avalonia.Size, System.Collections.Generic.IReadOnlyList<Avalonia.Media.FormattedTextStyleSpan>)' is present in the contract but not in the implementation.
MembersMustExist : Member 'public Avalonia.Platform.IFormattedTextImpl Avalonia.Platform.IPlatformRenderInterface.CreateFormattedText(System.String, Avalonia.Media.Typeface, System.Double, Avalonia.Media.TextAlignment, Avalonia.Media.TextWrapping, Avalonia.Size, System.Collections.Generic.IReadOnlyList<Avalonia.Media.FormattedTextStyleSpan>)' does not exist in the implementation but it does exist in the contract.
InterfacesShouldHaveSameMembers : Interface member 'public Avalonia.Platform.IGeometryImpl Avalonia.Platform.IPlatformRenderInterface.CreateGeometryGroup(Avalonia.Media.FillRule, System.Collections.Generic.IReadOnlyList<Avalonia.Media.Geometry>)' is present in the implementation but not in the contract. InterfacesShouldHaveSameMembers : Interface member 'public Avalonia.Platform.IGeometryImpl Avalonia.Platform.IPlatformRenderInterface.CreateGeometryGroup(Avalonia.Media.FillRule, System.Collections.Generic.IReadOnlyList<Avalonia.Media.Geometry>)' is present in the implementation but not in the contract.
InterfacesShouldHaveSameMembers : Interface member 'public Avalonia.Platform.IGlyphRunImpl Avalonia.Platform.IPlatformRenderInterface.CreateGlyphRun(Avalonia.Media.GlyphRun)' is present in the implementation but not in the contract. InterfacesShouldHaveSameMembers : Interface member 'public Avalonia.Platform.IGlyphRunImpl Avalonia.Platform.IPlatformRenderInterface.CreateGlyphRun(Avalonia.Media.GlyphRun)' is present in the implementation but not in the contract.
InterfacesShouldHaveSameMembers : Interface member 'public Avalonia.Platform.IGlyphRunImpl Avalonia.Platform.IPlatformRenderInterface.CreateGlyphRun(Avalonia.Media.GlyphRun, System.Double)' is present in the contract but not in the implementation. InterfacesShouldHaveSameMembers : Interface member 'public Avalonia.Platform.IGlyphRunImpl Avalonia.Platform.IPlatformRenderInterface.CreateGlyphRun(Avalonia.Media.GlyphRun, System.Double)' is present in the contract but not in the implementation.
@ -86,4 +140,9 @@ InterfacesShouldHaveSameMembers : Interface member 'public Avalonia.Size Avaloni
InterfacesShouldHaveSameMembers : Interface member 'public System.TimeSpan Avalonia.Platform.IPlatformSettings.TouchDoubleClickTime' is present in the implementation but not in the contract. InterfacesShouldHaveSameMembers : Interface member 'public System.TimeSpan Avalonia.Platform.IPlatformSettings.TouchDoubleClickTime' is present in the implementation but not in the contract.
InterfacesShouldHaveSameMembers : Interface member 'public Avalonia.Size Avalonia.Platform.IPlatformSettings.TouchDoubleClickSize.get()' is present in the implementation but not in the contract. InterfacesShouldHaveSameMembers : Interface member 'public Avalonia.Size Avalonia.Platform.IPlatformSettings.TouchDoubleClickSize.get()' is present in the implementation but not in the contract.
InterfacesShouldHaveSameMembers : Interface member 'public System.TimeSpan Avalonia.Platform.IPlatformSettings.TouchDoubleClickTime.get()' is present in the implementation but not in the contract. InterfacesShouldHaveSameMembers : Interface member 'public System.TimeSpan Avalonia.Platform.IPlatformSettings.TouchDoubleClickTime.get()' is present in the implementation but not in the contract.
Total Issues: 87 InterfacesShouldHaveSameMembers : Interface member 'public Avalonia.Media.TextFormatting.ShapedBuffer Avalonia.Platform.ITextShaperImpl.ShapeText(Avalonia.Utilities.ReadOnlySlice<System.Char>, Avalonia.Media.GlyphTypeface, System.Double, System.Globalization.CultureInfo, System.SByte)' is present in the implementation but not in the contract.
InterfacesShouldHaveSameMembers : Interface member 'public Avalonia.Media.GlyphRun Avalonia.Platform.ITextShaperImpl.ShapeText(Avalonia.Utilities.ReadOnlySlice<System.Char>, Avalonia.Media.Typeface, System.Double, System.Globalization.CultureInfo)' is present in the contract but not in the implementation.
MembersMustExist : Member 'public Avalonia.Media.GlyphRun Avalonia.Platform.ITextShaperImpl.ShapeText(Avalonia.Utilities.ReadOnlySlice<System.Char>, Avalonia.Media.Typeface, System.Double, System.Globalization.CultureInfo)' does not exist in the implementation but it does exist in the contract.
MembersMustExist : Member 'protected void Avalonia.Rendering.RendererBase.RenderFps(Avalonia.Platform.IDrawingContextImpl, Avalonia.Rect, System.Nullable<System.Int32>)' does not exist in the implementation but it does exist in the contract.
MembersMustExist : Member 'public void Avalonia.Utilities.ReadOnlySlice<T>..ctor(System.ReadOnlyMemory<T>, System.Int32, System.Int32)' does not exist in the implementation but it does exist in the contract.
Total Issues: 146

BIN
src/Avalonia.Visuals/Assets/BiDi.trie

Binary file not shown.

BIN
src/Avalonia.Visuals/Assets/UnicodeData.trie

Binary file not shown.

10
src/Avalonia.Visuals/Media/DrawingContext.cs

@ -226,17 +226,13 @@ namespace Avalonia.Media
/// <summary> /// <summary>
/// Draws text. /// Draws text.
/// </summary> /// </summary>
/// <param name="foreground">The foreground brush.</param>
/// <param name="origin">The upper-left corner of the text.</param> /// <param name="origin">The upper-left corner of the text.</param>
/// <param name="text">The text.</param> /// <param name="text">The text.</param>
public void DrawText(IBrush foreground, Point origin, FormattedText text) public void DrawText(FormattedText text, Point origin)
{ {
_ = text ?? throw new ArgumentNullException(nameof(text)); _ = text ?? throw new ArgumentNullException(nameof(text));
if (foreground != null) text.Draw(this, origin);
{
PlatformImpl.DrawText(foreground, origin, text.PlatformImpl);
}
} }
/// <summary> /// <summary>

1413
src/Avalonia.Visuals/Media/FormattedText.cs

File diff suppressed because it is too large

29
src/Avalonia.Visuals/Media/FormattedTextLine.cs

@ -1,29 +0,0 @@
namespace Avalonia.Media
{
/// <summary>
/// Stores information about a line of <see cref="FormattedText"/>.
/// </summary>
public class FormattedTextLine
{
/// <summary>
/// Initializes a new instance of the <see cref="FormattedTextLine"/> class.
/// </summary>
/// <param name="length">The length of the line, in characters.</param>
/// <param name="height">The height of the line, in pixels.</param>
public FormattedTextLine(int length, double height)
{
Length = length;
Height = height;
}
/// <summary>
/// Gets the length of the line, in characters.
/// </summary>
public int Length { get; }
/// <summary>
/// Gets the height of the line, in pixels.
/// </summary>
public double Height { get; }
}
}

39
src/Avalonia.Visuals/Media/FormattedTextStyleSpan.cs

@ -1,39 +0,0 @@
namespace Avalonia.Media
{
/// <summary>
/// Describes the formatting for a span of text in a <see cref="FormattedText"/> object.
/// </summary>
public class FormattedTextStyleSpan
{
/// <summary>
/// Initializes a new instance of the <see cref="FormattedTextStyleSpan"/> class.
/// </summary>
/// <param name="startIndex">The index of the first character in the span.</param>
/// <param name="length">The length of the span.</param>
/// <param name="foregroundBrush">The span's foreground brush.</param>
public FormattedTextStyleSpan(
int startIndex,
int length,
IBrush? foregroundBrush = null)
{
StartIndex = startIndex;
Length = length;
ForegroundBrush = foregroundBrush;
}
/// <summary>
/// Gets the index of the first character in the span.
/// </summary>
public int StartIndex { get; }
/// <summary>
/// Gets the length of the span.
/// </summary>
public int Length { get; }
/// <summary>
/// Gets the span's foreground brush.
/// </summary>
public IBrush? ForegroundBrush { get; }
}
}

290
src/Avalonia.Visuals/Media/GlyphRun.cs

@ -11,8 +11,8 @@ namespace Avalonia.Media
/// </summary> /// </summary>
public sealed class GlyphRun : IDisposable public sealed class GlyphRun : IDisposable
{ {
private static readonly IComparer<ushort> s_ascendingComparer = Comparer<ushort>.Default; private static readonly IComparer<int> s_ascendingComparer = Comparer<int>.Default;
private static readonly IComparer<ushort> s_descendingComparer = new ReverseComparer<ushort>(); private static readonly IComparer<int> s_descendingComparer = new ReverseComparer<int>();
private IGlyphRunImpl? _glyphRunImpl; private IGlyphRunImpl? _glyphRunImpl;
private GlyphTypeface _glyphTypeface; private GlyphTypeface _glyphTypeface;
@ -21,12 +21,13 @@ namespace Avalonia.Media
private Point? _baselineOrigin; private Point? _baselineOrigin;
private GlyphRunMetrics? _glyphRunMetrics; private GlyphRunMetrics? _glyphRunMetrics;
private ReadOnlySlice<ushort> _glyphIndices;
private ReadOnlySlice<double> _glyphAdvances;
private ReadOnlySlice<Vector> _glyphOffsets;
private ReadOnlySlice<ushort> _glyphClusters;
private ReadOnlySlice<char> _characters; private ReadOnlySlice<char> _characters;
private IReadOnlyList<ushort> _glyphIndices;
private IReadOnlyList<double>? _glyphAdvances;
private IReadOnlyList<Vector>? _glyphOffsets;
private IReadOnlyList<int>? _glyphClusters;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="GlyphRun"/> class by specifying properties of the class. /// Initializes a new instance of the <see cref="GlyphRun"/> class by specifying properties of the class.
/// </summary> /// </summary>
@ -41,25 +42,25 @@ namespace Avalonia.Media
public GlyphRun( public GlyphRun(
GlyphTypeface glyphTypeface, GlyphTypeface glyphTypeface,
double fontRenderingEmSize, double fontRenderingEmSize,
ReadOnlySlice<ushort> glyphIndices, ReadOnlySlice<char> characters,
ReadOnlySlice<double> glyphAdvances = default, IReadOnlyList<ushort> glyphIndices,
ReadOnlySlice<Vector> glyphOffsets = default, IReadOnlyList<double>? glyphAdvances = null,
ReadOnlySlice<char> characters = default, IReadOnlyList<Vector>? glyphOffsets = null,
ReadOnlySlice<ushort> glyphClusters = default, IReadOnlyList<int>? glyphClusters = null,
int biDiLevel = 0) int biDiLevel = 0)
{ {
_glyphTypeface = glyphTypeface; _glyphTypeface = glyphTypeface;
FontRenderingEmSize = fontRenderingEmSize; FontRenderingEmSize = fontRenderingEmSize;
GlyphIndices = glyphIndices; Characters = characters;
_glyphIndices = glyphIndices;
GlyphAdvances = glyphAdvances; GlyphAdvances = glyphAdvances;
GlyphOffsets = glyphOffsets; GlyphOffsets = glyphOffsets;
Characters = characters;
GlyphClusters = glyphClusters; GlyphClusters = glyphClusters;
BiDiLevel = biDiLevel; BiDiLevel = biDiLevel;
@ -114,7 +115,7 @@ namespace Avalonia.Media
/// <summary> /// <summary>
/// Gets or sets an array of <see cref="ushort"/> values that represent the glyph indices in the rendering physical font. /// Gets or sets an array of <see cref="ushort"/> values that represent the glyph indices in the rendering physical font.
/// </summary> /// </summary>
public ReadOnlySlice<ushort> GlyphIndices public IReadOnlyList<ushort> GlyphIndices
{ {
get => _glyphIndices; get => _glyphIndices;
set => Set(ref _glyphIndices, value); set => Set(ref _glyphIndices, value);
@ -123,7 +124,7 @@ namespace Avalonia.Media
/// <summary> /// <summary>
/// Gets or sets an array of <see cref="double"/> values that represent the advances corresponding to the glyph indices. /// Gets or sets an array of <see cref="double"/> values that represent the advances corresponding to the glyph indices.
/// </summary> /// </summary>
public ReadOnlySlice<double> GlyphAdvances public IReadOnlyList<double>? GlyphAdvances
{ {
get => _glyphAdvances; get => _glyphAdvances;
set => Set(ref _glyphAdvances, value); set => Set(ref _glyphAdvances, value);
@ -132,7 +133,7 @@ namespace Avalonia.Media
/// <summary> /// <summary>
/// Gets or sets an array of <see cref="Vector"/> values representing the offsets of the glyphs in the <see cref="GlyphRun"/>. /// Gets or sets an array of <see cref="Vector"/> values representing the offsets of the glyphs in the <see cref="GlyphRun"/>.
/// </summary> /// </summary>
public ReadOnlySlice<Vector> GlyphOffsets public IReadOnlyList<Vector>? GlyphOffsets
{ {
get => _glyphOffsets; get => _glyphOffsets;
set => Set(ref _glyphOffsets, value); set => Set(ref _glyphOffsets, value);
@ -150,7 +151,7 @@ namespace Avalonia.Media
/// <summary> /// <summary>
/// Gets or sets a list of <see cref="int"/> values representing a mapping from character index to glyph index. /// Gets or sets a list of <see cref="int"/> values representing a mapping from character index to glyph index.
/// </summary> /// </summary>
public ReadOnlySlice<ushort> GlyphClusters public IReadOnlyList<int>? GlyphClusters
{ {
get => _glyphClusters; get => _glyphClusters;
set => Set(ref _glyphClusters, value); set => Set(ref _glyphClusters, value);
@ -202,34 +203,73 @@ namespace Avalonia.Media
/// </returns> /// </returns>
public double GetDistanceFromCharacterHit(CharacterHit characterHit) public double GetDistanceFromCharacterHit(CharacterHit characterHit)
{ {
var characterIndex = characterHit.FirstCharacterIndex + characterHit.TrailingLength;
var distance = 0.0; var distance = 0.0;
if (characterHit.FirstCharacterIndex + characterHit.TrailingLength > Characters.End) if (IsLeftToRight)
{ {
return Size.Width; if (GlyphClusters != null)
} {
if (characterIndex < GlyphClusters[0])
var glyphIndex = FindGlyphIndex(characterHit.FirstCharacterIndex); {
return 0;
}
if (!GlyphClusters.IsEmpty) if (characterIndex > GlyphClusters[GlyphClusters.Count - 1])
{ {
var currentCluster = GlyphClusters[glyphIndex]; return Metrics.WidthIncludingTrailingWhitespace;
}
}
if (characterHit.TrailingLength > 0) var glyphIndex = FindGlyphIndex(characterIndex);
if (GlyphClusters != null)
{ {
while (glyphIndex < GlyphClusters.Length && GlyphClusters[glyphIndex] == currentCluster) var currentCluster = GlyphClusters[glyphIndex];
//Move to the end of the glyph cluster
if (characterHit.TrailingLength > 0)
{ {
glyphIndex++; while (glyphIndex + 1 < GlyphClusters.Count && GlyphClusters[glyphIndex + 1] == currentCluster)
{
glyphIndex++;
}
} }
} }
}
for (var i = 0; i < glyphIndex; i++) for (var i = 0; i < glyphIndex; i++)
{ {
distance += GetGlyphAdvance(i); distance += GetGlyphAdvance(i, out _);
}
return distance;
} }
else
{
//RightToLeft
var glyphIndex = FindGlyphIndex(characterIndex);
if (GlyphClusters != null)
{
if (characterIndex > GlyphClusters[0])
{
return 0;
}
if (characterIndex <= GlyphClusters[GlyphClusters.Count - 1])
{
return Size.Width;
}
}
return distance; for (var i = glyphIndex + 1; i < GlyphIndices.Count; i++)
{
distance += GetGlyphAdvance(i, out _);
}
return Size.Width - distance;
}
} }
/// <summary> /// <summary>
@ -243,50 +283,86 @@ namespace Avalonia.Media
/// </returns> /// </returns>
public CharacterHit GetCharacterHitFromDistance(double distance, out bool isInside) public CharacterHit GetCharacterHitFromDistance(double distance, out bool isInside)
{ {
var characterIndex = 0;
// Before // Before
if (distance < 0) if (distance <= 0)
{ {
isInside = false; isInside = false;
var firstCharacterHit = FindNearestCharacterHit(_glyphClusters[0], out _); if(GlyphClusters != null)
{
characterIndex = GlyphClusters[characterIndex];
}
var firstCharacterHit = FindNearestCharacterHit(characterIndex, out _);
return IsLeftToRight ? new CharacterHit(firstCharacterHit.FirstCharacterIndex) : firstCharacterHit; return IsLeftToRight ? new CharacterHit(firstCharacterHit.FirstCharacterIndex) : firstCharacterHit;
} }
//After //After
if (distance > Size.Width) if (distance >= Size.Width)
{ {
isInside = false; isInside = false;
var lastCharacterHit = FindNearestCharacterHit(_glyphClusters[_glyphClusters.Length - 1], out _); characterIndex = GlyphIndices.Count - 1;
if(GlyphClusters != null)
{
characterIndex = GlyphClusters[characterIndex];
}
var lastCharacterHit = FindNearestCharacterHit(characterIndex, out _);
return IsLeftToRight ? lastCharacterHit : new CharacterHit(lastCharacterHit.FirstCharacterIndex); return IsLeftToRight ? lastCharacterHit : new CharacterHit(lastCharacterHit.FirstCharacterIndex);
} }
//Within //Within
var currentX = 0.0; var currentX = 0d;
var index = 0;
for (; index < GlyphIndices.Length - Metrics.NewlineLength; index++) if (IsLeftToRight)
{ {
var advance = GetGlyphAdvance(index); for (var index = 0; index < GlyphIndices.Count; index++)
if (currentX + advance >= distance)
{ {
break; var advance = GetGlyphAdvance(index, out var cluster);
}
characterIndex = cluster;
if (distance > currentX && distance <= currentX + advance)
{
break;
}
currentX += advance; currentX += advance;
}
} }
else
{
currentX = Size.Width;
var characterHit = for (var index = GlyphIndices.Count - 1; index >= 0; index--)
FindNearestCharacterHit(GlyphClusters.IsEmpty ? index : GlyphClusters[index], out var width); {
var advance = GetGlyphAdvance(index, out var cluster);
var offset = GetDistanceFromCharacterHit(new CharacterHit(characterHit.FirstCharacterIndex)); characterIndex = cluster;
if (currentX - advance < distance)
{
break;
}
currentX -= advance;
}
}
isInside = true; isInside = true;
var isTrailing = distance > offset + width / 2; var characterHit = FindNearestCharacterHit(characterIndex, out var width);
var delta = width / 2;
var offset = IsLeftToRight ? distance - currentX : currentX - distance;
var isTrailing = offset > delta;
return isTrailing ? characterHit : new CharacterHit(characterHit.FirstCharacterIndex); return isTrailing ? characterHit : new CharacterHit(characterHit.FirstCharacterIndex);
} }
@ -303,13 +379,21 @@ namespace Avalonia.Media
{ {
if (characterHit.TrailingLength == 0) if (characterHit.TrailingLength == 0)
{ {
return FindNearestCharacterHit(characterHit.FirstCharacterIndex, out _); characterHit = FindNearestCharacterHit(characterHit.FirstCharacterIndex, out _);
var textPosition = characterHit.FirstCharacterIndex + characterHit.TrailingLength;
return textPosition > _characters.End ?
characterHit :
new CharacterHit(characterHit.FirstCharacterIndex + characterHit.TrailingLength);
} }
var nextCharacterHit = var nextCharacterHit =
FindNearestCharacterHit(characterHit.FirstCharacterIndex + characterHit.TrailingLength, out _); FindNearestCharacterHit(characterHit.FirstCharacterIndex + characterHit.TrailingLength, out _);
return new CharacterHit(nextCharacterHit.FirstCharacterIndex); return nextCharacterHit == characterHit ?
characterHit :
new CharacterHit(nextCharacterHit.FirstCharacterIndex);
} }
/// <summary> /// <summary>
@ -327,9 +411,9 @@ namespace Avalonia.Media
return new CharacterHit(characterHit.FirstCharacterIndex); return new CharacterHit(characterHit.FirstCharacterIndex);
} }
return characterHit.FirstCharacterIndex == Characters.Start ? var previousCharacterHit = FindNearestCharacterHit(characterHit.FirstCharacterIndex - 1, out _);
new CharacterHit(Characters.Start) :
FindNearestCharacterHit(characterHit.FirstCharacterIndex - 1, out _); return new CharacterHit(previousCharacterHit.FirstCharacterIndex);
} }
/// <summary> /// <summary>
@ -341,7 +425,7 @@ namespace Avalonia.Media
/// </returns> /// </returns>
public int FindGlyphIndex(int characterIndex) public int FindGlyphIndex(int characterIndex)
{ {
if (GlyphClusters.IsEmpty) if (GlyphClusters == null)
{ {
return characterIndex; return characterIndex;
} }
@ -353,16 +437,16 @@ namespace Avalonia.Media
return 0; return 0;
} }
if (characterIndex > GlyphClusters[GlyphClusters.Length - 1]) if (characterIndex > GlyphClusters[GlyphClusters.Count - 1])
{ {
return _glyphClusters.Length - 1; return GlyphClusters.Count - 1;
} }
} }
else else
{ {
if (characterIndex < GlyphClusters[GlyphClusters.Length - 1]) if (characterIndex < GlyphClusters[GlyphClusters.Count - 1])
{ {
return _glyphClusters.Length - 1; return GlyphClusters.Count - 1;
} }
if (characterIndex > GlyphClusters[0]) if (characterIndex > GlyphClusters[0])
@ -373,10 +457,10 @@ namespace Avalonia.Media
var comparer = IsLeftToRight ? s_ascendingComparer : s_descendingComparer; var comparer = IsLeftToRight ? s_ascendingComparer : s_descendingComparer;
var clusters = GlyphClusters.Buffer.Span; var clusters = GlyphClusters;
// Find the start of the cluster at the character index. // Find the start of the cluster at the character index.
var start = clusters.BinarySearch((ushort)characterIndex, comparer); var start = clusters.BinarySearch(characterIndex, comparer);
// No cluster found. // No cluster found.
if (start < 0) if (start < 0)
@ -385,7 +469,7 @@ namespace Avalonia.Media
{ {
characterIndex--; characterIndex--;
start = clusters.BinarySearch((ushort)characterIndex, comparer); start = clusters.BinarySearch(characterIndex, comparer);
} }
if (start < 0) if (start < 0)
@ -403,7 +487,7 @@ namespace Avalonia.Media
} }
else else
{ {
while (start + 1 < clusters.Length && clusters[start + 1] == clusters[start]) while (start + 1 < clusters.Count && clusters[start + 1] == clusters[start])
{ {
start++; start++;
} }
@ -426,9 +510,9 @@ namespace Avalonia.Media
var start = FindGlyphIndex(index); var start = FindGlyphIndex(index);
if (GlyphClusters.IsEmpty) if (GlyphClusters == null)
{ {
width = GetGlyphAdvance(index); width = GetGlyphAdvance(index, out _);
return new CharacterHit(start, 1); return new CharacterHit(start, 1);
} }
@ -441,13 +525,13 @@ namespace Avalonia.Media
while (nextCluster == cluster) while (nextCluster == cluster)
{ {
width += GetGlyphAdvance(currentIndex); width += GetGlyphAdvance(currentIndex, out _);
if (IsLeftToRight) if (IsLeftToRight)
{ {
currentIndex++; currentIndex++;
if (currentIndex == GlyphClusters.Length) if (currentIndex == GlyphClusters.Count)
{ {
break; break;
} }
@ -483,10 +567,13 @@ namespace Avalonia.Media
/// Gets a glyph's width. /// Gets a glyph's width.
/// </summary> /// </summary>
/// <param name="index">The glyph index.</param> /// <param name="index">The glyph index.</param>
/// <param name="cluster">The current cluster.</param>
/// <returns>The glyph's width.</returns> /// <returns>The glyph's width.</returns>
private double GetGlyphAdvance(int index) private double GetGlyphAdvance(int index, out int cluster)
{ {
if (!GlyphAdvances.IsEmpty) cluster = GlyphClusters != null ? GlyphClusters[index] : index;
if (GlyphAdvances != null)
{ {
return GlyphAdvances[index]; return GlyphAdvances[index];
} }
@ -508,42 +595,51 @@ namespace Avalonia.Media
private GlyphRunMetrics CreateGlyphRunMetrics() private GlyphRunMetrics CreateGlyphRunMetrics()
{ {
var height = (GlyphTypeface.Descent - GlyphTypeface.Ascent + GlyphTypeface.LineGap) * Scale; var height = (GlyphTypeface.Descent - GlyphTypeface.Ascent + GlyphTypeface.LineGap) * Scale;
var widthIncludingTrailingWhitespace = 0d; var widthIncludingTrailingWhitespace = 0d;
var width = 0d;
var trailingWhitespaceLength = GetTrailingWhitespaceLength(out var newLineLength); var trailingWhitespaceLength = GetTrailingWhitespaceLength(out var newLineLength, out var glyphCount);
for (var index = 0; index < _glyphIndices.Length; index++) for (var index = 0; index < GlyphIndices.Count; index++)
{ {
var advance = GetGlyphAdvance(index); var advance = GetGlyphAdvance(index, out _);
widthIncludingTrailingWhitespace += advance; widthIncludingTrailingWhitespace += advance;
}
var width = widthIncludingTrailingWhitespace;
if (index > _glyphIndices.Length - 1 - trailingWhitespaceLength) if (IsLeftToRight)
{
for (var index = GlyphIndices.Count - glyphCount; index <GlyphIndices.Count; index++)
{ {
continue; width -= GetGlyphAdvance(index, out _);
}
}
else
{
for (var index = 0; index < glyphCount; index++)
{
width -= GetGlyphAdvance(index, out _);
} }
width += advance;
} }
return new GlyphRunMetrics(width, widthIncludingTrailingWhitespace, trailingWhitespaceLength, newLineLength, return new GlyphRunMetrics(width, widthIncludingTrailingWhitespace, trailingWhitespaceLength, newLineLength,
height); height);
} }
private int GetTrailingWhitespaceLength(out int newLineLength) private int GetTrailingWhitespaceLength(out int newLineLength, out int glyphCount)
{ {
glyphCount = 0;
newLineLength = 0; newLineLength = 0;
if (_characters.IsEmpty) if (Characters.IsEmpty)
{ {
return 0; return 0;
} }
var trailingWhitespaceLength = 0; var trailingWhitespaceLength = 0;
if (_glyphClusters.IsEmpty) if (GlyphClusters == null)
{ {
for (var i = _characters.Length - 1; i >= 0;) for (var i = _characters.Length - 1; i >= 0;)
{ {
@ -562,16 +658,26 @@ namespace Avalonia.Media
trailingWhitespaceLength++; trailingWhitespaceLength++;
i -= count; i -= count;
glyphCount++;
} }
} }
else else
{ {
for (var i = _glyphClusters.Length - 1; i >= 0; i--) for (var i = GlyphClusters.Count - 1; i >= 0; i--)
{ {
var cluster = _glyphClusters[i]; var cluster = GlyphClusters[i];
var codepointIndex = IsLeftToRight ? cluster - _characters.Start : _characters.End - cluster; var codepointIndex = IsLeftToRight ? cluster - _characters.Start : _characters.End - cluster;
if (codepointIndex < 0)
{
trailingWhitespaceLength = _characters.Length;
glyphCount = GlyphClusters.Count;
break;
}
var codepoint = Codepoint.ReadAt(_characters, codepointIndex, out _); var codepoint = Codepoint.ReadAt(_characters, codepointIndex, out _);
if (!codepoint.IsWhiteSpace) if (!codepoint.IsWhiteSpace)
@ -585,6 +691,8 @@ namespace Avalonia.Media
} }
trailingWhitespaceLength++; trailingWhitespaceLength++;
glyphCount++;
} }
} }
@ -610,19 +718,19 @@ namespace Avalonia.Media
/// </summary> /// </summary>
private void Initialize() private void Initialize()
{ {
if (GlyphIndices.Length == 0) if (GlyphIndices == null)
{ {
throw new InvalidOperationException(); throw new InvalidOperationException();
} }
var glyphCount = GlyphIndices.Length; var glyphCount = GlyphIndices.Count;
if (GlyphAdvances.Length > 0 && GlyphAdvances.Length != glyphCount) if (GlyphAdvances != null && GlyphAdvances.Count > 0 && GlyphAdvances.Count != glyphCount)
{ {
throw new InvalidOperationException(); throw new InvalidOperationException();
} }
if (GlyphOffsets.Length > 0 && GlyphOffsets.Length != glyphCount) if (GlyphOffsets != null && GlyphOffsets.Count > 0 && GlyphOffsets.Count != glyphCount)
{ {
throw new InvalidOperationException(); throw new InvalidOperationException();
} }

2
src/Avalonia.Visuals/Media/GlyphTypeface.cs

@ -5,8 +5,6 @@ namespace Avalonia.Media
{ {
public sealed class GlyphTypeface : IDisposable public sealed class GlyphTypeface : IDisposable
{ {
public const int InvisibleGlyph = 3;
public GlyphTypeface(Typeface typeface) public GlyphTypeface(Typeface typeface)
: this(FontManager.Current.PlatformImpl.CreateGlyphTypeface(typeface)) : this(FontManager.Current.PlatformImpl.CreateGlyphTypeface(typeface))
{ {

25
src/Avalonia.Visuals/Media/TextDecoration.cs

@ -154,11 +154,12 @@ namespace Avalonia.Media
/// Draws the <see cref="TextDecoration"/> at given origin. /// Draws the <see cref="TextDecoration"/> at given origin.
/// </summary> /// </summary>
/// <param name="drawingContext">The drawing context.</param> /// <param name="drawingContext">The drawing context.</param>
/// <param name="shapedTextCharacters">The shaped characters that are decorated.</param> /// <param name="glyphRun">The decorated run.</param>
internal void Draw(DrawingContext drawingContext, ShapedTextCharacters shapedTextCharacters) /// <param name="fontMetrics">The font metrics of the decorated run.</param>
/// <param name="defaultBrush">The default brush that is used to draw the decoration.</param>
internal void Draw(DrawingContext drawingContext, GlyphRun glyphRun, FontMetrics fontMetrics, IBrush defaultBrush)
{ {
var fontRenderingEmSize = shapedTextCharacters.Properties.FontRenderingEmSize; var baselineOrigin = glyphRun.BaselineOrigin;
var fontMetrics = shapedTextCharacters.FontMetrics;
var thickness = StrokeThickness; var thickness = StrokeThickness;
switch (StrokeThicknessUnit) switch (StrokeThicknessUnit)
@ -176,7 +177,7 @@ namespace Avalonia.Media
break; break;
case TextDecorationUnit.FontRenderingEmSize: case TextDecorationUnit.FontRenderingEmSize:
thickness = fontRenderingEmSize * thickness; thickness = fontMetrics.FontRenderingEmSize * thickness;
break; break;
} }
@ -185,32 +186,30 @@ namespace Avalonia.Media
switch (Location) switch (Location)
{ {
case TextDecorationLocation.Baseline: case TextDecorationLocation.Baseline:
origin += shapedTextCharacters.GlyphRun.BaselineOrigin; origin += glyphRun.BaselineOrigin;
break; break;
case TextDecorationLocation.Strikethrough: case TextDecorationLocation.Strikethrough:
origin += new Point(shapedTextCharacters.GlyphRun.BaselineOrigin.X, origin += new Point(baselineOrigin.X, baselineOrigin.Y + fontMetrics.StrikethroughPosition);
shapedTextCharacters.GlyphRun.BaselineOrigin.Y + fontMetrics.StrikethroughPosition);
break; break;
case TextDecorationLocation.Underline: case TextDecorationLocation.Underline:
origin += new Point(shapedTextCharacters.GlyphRun.BaselineOrigin.X, origin += new Point(baselineOrigin.X, baselineOrigin.Y + fontMetrics.UnderlinePosition);
shapedTextCharacters.GlyphRun.BaselineOrigin.Y + fontMetrics.UnderlinePosition);
break; break;
} }
switch (StrokeOffsetUnit) switch (StrokeOffsetUnit)
{ {
case TextDecorationUnit.FontRenderingEmSize: case TextDecorationUnit.FontRenderingEmSize:
origin += new Point(0, StrokeOffset * fontRenderingEmSize); origin += new Point(0, StrokeOffset * fontMetrics.FontRenderingEmSize);
break; break;
case TextDecorationUnit.Pixel: case TextDecorationUnit.Pixel:
origin += new Point(0, StrokeOffset); origin += new Point(0, StrokeOffset);
break; break;
} }
var pen = new Pen(Stroke ?? shapedTextCharacters.Properties.ForegroundBrush, thickness, var pen = new Pen(Stroke ?? defaultBrush, thickness,
new DashStyle(StrokeDashArray, StrokeDashOffset), StrokeLineCap); new DashStyle(StrokeDashArray, StrokeDashOffset), StrokeLineCap);
drawingContext.DrawLine(pen, origin, origin + new Point(shapedTextCharacters.Size.Width, 0)); drawingContext.DrawLine(pen, origin, origin + new Point(glyphRun.Size.Width, 0));
} }
} }
} }

11
src/Avalonia.Visuals/Media/TextFormatting/FontMetrics.cs

@ -5,11 +5,13 @@
/// </summary> /// </summary>
public readonly struct FontMetrics public readonly struct FontMetrics
{ {
public FontMetrics(Typeface typeface, double fontSize) public FontMetrics(Typeface typeface, double fontRenderingEmSize)
{ {
var glyphTypeface = typeface.GlyphTypeface; var glyphTypeface = typeface.GlyphTypeface;
var scale = fontSize / glyphTypeface.DesignEmHeight; var scale = fontRenderingEmSize / glyphTypeface.DesignEmHeight;
FontRenderingEmSize = fontRenderingEmSize;
Ascent = glyphTypeface.Ascent * scale; Ascent = glyphTypeface.Ascent * scale;
@ -28,6 +30,11 @@
StrikethroughPosition = glyphTypeface.StrikethroughPosition * scale; StrikethroughPosition = glyphTypeface.StrikethroughPosition * scale;
} }
/// <summary>
/// Em size of font used to format and display text
/// </summary>
public double FontRenderingEmSize { get; }
/// <summary> /// <summary>
/// Gets the recommended distance above the baseline. /// Gets the recommended distance above the baseline.
/// </summary> /// </summary>

24
tests/Avalonia.Skia.UnitTests/Media/TextFormatting/FormattedTextSource.cs → src/Avalonia.Visuals/Media/TextFormatting/FormattedTextSource.cs

@ -1,27 +1,27 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using Avalonia.Media.TextFormatting; using Avalonia.Media.TextFormatting.Unicode;
using Avalonia.Utilities; using Avalonia.Utilities;
namespace Avalonia.Skia.UnitTests.Media.TextFormatting namespace Avalonia.Media.TextFormatting
{ {
internal readonly struct FormattedTextSource : ITextSource internal readonly struct FormattedTextSource : ITextSource
{ {
private readonly ReadOnlySlice<char> _text; private readonly ReadOnlySlice<char> _text;
private readonly TextRunProperties _defaultProperties; private readonly TextRunProperties _defaultProperties;
private readonly IReadOnlyList<ValueSpan<TextRunProperties>> _textModifier; private readonly IReadOnlyList<ValueSpan<TextRunProperties>>? _textModifier;
public FormattedTextSource(ReadOnlySlice<char> text, TextRunProperties defaultProperties, public FormattedTextSource(ReadOnlySlice<char> text, TextRunProperties defaultProperties,
IReadOnlyList<ValueSpan<TextRunProperties>> textModifier) IReadOnlyList<ValueSpan<TextRunProperties>>? textModifier)
{ {
_text = text; _text = text;
_defaultProperties = defaultProperties; _defaultProperties = defaultProperties;
_textModifier = textModifier; _textModifier = textModifier;
} }
public TextRun GetTextRun(int textSourceIndex) public TextRun? GetTextRun(int textSourceIndex)
{ {
if (textSourceIndex > _text.End) if (textSourceIndex > _text.Length)
{ {
return null; return null;
} }
@ -48,7 +48,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
/// The created text style run. /// The created text style run.
/// </returns> /// </returns>
private static ValueSpan<TextRunProperties> CreateTextStyleRun(ReadOnlySlice<char> text, private static ValueSpan<TextRunProperties> CreateTextStyleRun(ReadOnlySlice<char> text,
TextRunProperties defaultProperties, IReadOnlyList<ValueSpan<TextRunProperties>> textModifier) TextRunProperties defaultProperties, IReadOnlyList<ValueSpan<TextRunProperties>>? textModifier)
{ {
if (textModifier == null || textModifier.Count == 0) if (textModifier == null || textModifier.Count == 0)
{ {
@ -69,7 +69,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
var textRange = new TextRange(propertiesOverride.Start, propertiesOverride.Length); var textRange = new TextRange(propertiesOverride.Start, propertiesOverride.Length);
if (textRange.End < text.Start) if (textRange.Start + textRange.Length < text.Start)
{ {
continue; continue;
} }
@ -90,7 +90,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
} }
} }
length += Math.Min(text.Length - length, textRange.Length); length += Math.Max(0, textRange.Start + textRange.Length - text.Start);
if (hasOverride) if (hasOverride)
{ {
@ -110,6 +110,12 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
} }
} }
if (length == 0 && currentProperties != defaultProperties)
{
currentProperties = defaultProperties;
length = text.Length;
}
if (length != text.Length) if (length != text.Length)
{ {
text = text.Take(length); text = text.Take(length);

5
src/Avalonia.Visuals/Media/TextFormatting/ShapeableTextCharacters.cs

@ -7,11 +7,12 @@ namespace Avalonia.Media.TextFormatting
/// </summary> /// </summary>
public sealed class ShapeableTextCharacters : TextRun public sealed class ShapeableTextCharacters : TextRun
{ {
public ShapeableTextCharacters(ReadOnlySlice<char> text, TextRunProperties properties) public ShapeableTextCharacters(ReadOnlySlice<char> text, TextRunProperties properties, sbyte biDiLevel)
{ {
TextSourceLength = text.Length; TextSourceLength = text.Length;
Text = text; Text = text;
Properties = properties; Properties = properties;
BidiLevel = biDiLevel;
} }
public override int TextSourceLength { get; } public override int TextSourceLength { get; }
@ -19,5 +20,7 @@ namespace Avalonia.Media.TextFormatting
public override ReadOnlySlice<char> Text { get; } public override ReadOnlySlice<char> Text { get; }
public override TextRunProperties Properties { get; } public override TextRunProperties Properties { get; }
public sbyte BidiLevel { get; }
} }
} }

293
src/Avalonia.Visuals/Media/TextFormatting/ShapedBuffer.cs

@ -0,0 +1,293 @@
using System;
using System.Collections.Generic;
using Avalonia.Utilities;
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, GlyphTypeface glyphTypeface, double fontRenderingEmSize, sbyte bidiLevel)
: this(text, new GlyphInfo[length], glyphTypeface, fontRenderingEmSize, bidiLevel)
{
}
internal ShapedBuffer(ReadOnlySlice<char> text, ArraySlice<GlyphInfo> glyphInfos, GlyphTypeface glyphTypeface, double fontRenderingEmSize, sbyte bidiLevel)
{
Text = text;
GlyphInfos = glyphInfos;
GlyphTypeface = glyphTypeface;
FontRenderingEmSize = fontRenderingEmSize;
BidiLevel = bidiLevel;
}
internal ArraySlice<GlyphInfo> GlyphInfos { get; }
public ReadOnlySlice<char> Text { get; }
public int Length => GlyphInfos.Length;
public GlyphTypeface GlyphTypeface { get; }
public double FontRenderingEmSize { get; }
public sbyte BidiLevel { get; }
public bool IsLeftToRight => (BidiLevel & 1) == 0;
public IReadOnlyList<ushort> GlyphIndices => new GlyphIndexList(GlyphInfos);
public IReadOnlyList<int> GlyphClusters => new GlyphClusterList(GlyphInfos);
public IReadOnlyList<double> GlyphAdvances => new GlyphAdvanceList(GlyphInfos);
public IReadOnlyList<Vector> GlyphOffsets => new GlyphOffsetList(GlyphInfos);
/// <summary>
/// Finds a glyph index for given character index.
/// </summary>
/// <param name="characterIndex">The character index.</param>
/// <returns>
/// The glyph index.
/// </returns>
private int FindGlyphIndex(int characterIndex)
{
if (characterIndex < GlyphInfos[0].GlyphCluster)
{
return 0;
}
if (characterIndex > GlyphInfos[GlyphInfos.Length - 1].GlyphCluster)
{
return GlyphInfos.Length - 1;
}
var comparer = s_clusterComparer;
var clusters = GlyphInfos.Span;
var searchValue = new GlyphInfo(0, characterIndex);
var start = clusters.BinarySearch(searchValue, comparer);
if (start < 0)
{
while (characterIndex > 0 && start < 0)
{
characterIndex--;
searchValue = new GlyphInfo(0, characterIndex);
start = clusters.BinarySearch(searchValue, comparer);
}
if (start < 0)
{
return -1;
}
}
while (start > 0 && clusters[start - 1].GlyphCluster == clusters[start].GlyphCluster)
{
start--;
}
return start;
}
/// <summary>
/// Splits the <see cref="TextRun"/> at specified length.
/// </summary>
/// <param name="length">The length.</param>
/// <returns>The split result.</returns>
internal SplitResult<ShapedBuffer> Split(int length)
{
var glyphCount = FindGlyphIndex(Text.Start + length);
if (Text.Length == length)
{
return new SplitResult<ShapedBuffer>(this, null);
}
if (Text.Length == glyphCount)
{
return new SplitResult<ShapedBuffer>(this, null);
}
var first = new ShapedBuffer(Text.Take(length), GlyphInfos.Take(glyphCount), GlyphTypeface, FontRenderingEmSize, BidiLevel);
var second = new ShapedBuffer(Text.Skip(length), GlyphInfos.Skip(glyphCount), GlyphTypeface, FontRenderingEmSize, BidiLevel);
return new SplitResult<ShapedBuffer>(first, second);
}
int ICollection<GlyphInfo>.Count => throw new NotImplementedException();
bool ICollection<GlyphInfo>.IsReadOnly => true;
public GlyphInfo this[int index]
{
get => GlyphInfos[index];
set => GlyphInfos[index] = value;
}
int IList<GlyphInfo>.IndexOf(GlyphInfo item)
{
throw new NotImplementedException();
}
void IList<GlyphInfo>.Insert(int index, GlyphInfo item)
{
throw new NotImplementedException();
}
void IList<GlyphInfo>.RemoveAt(int index)
{
throw new NotImplementedException();
}
void ICollection<GlyphInfo>.Add(GlyphInfo item)
{
throw new NotImplementedException();
}
void ICollection<GlyphInfo>.Clear()
{
throw new NotImplementedException();
}
bool ICollection<GlyphInfo>.Contains(GlyphInfo item)
{
throw new NotImplementedException();
}
void ICollection<GlyphInfo>.CopyTo(GlyphInfo[] array, int arrayIndex)
{
throw new NotImplementedException();
}
bool ICollection<GlyphInfo>.Remove(GlyphInfo item)
{
throw new NotImplementedException();
}
public IEnumerator<GlyphInfo> GetEnumerator() => GlyphInfos.GetEnumerator();
System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() => GetEnumerator();
private class CompareClusters : IComparer<GlyphInfo>
{
private static readonly Comparer<int> s_intClusterComparer = Comparer<int>.Default;
public int Compare(GlyphInfo x, GlyphInfo y)
{
return s_intClusterComparer.Compare(x.GlyphCluster, y.GlyphCluster);
}
}
private readonly struct GlyphAdvanceList : IReadOnlyList<double>
{
private readonly ArraySlice<GlyphInfo> _glyphInfos;
public GlyphAdvanceList(ArraySlice<GlyphInfo> glyphInfos)
{
_glyphInfos = glyphInfos;
}
public double this[int index] => _glyphInfos[index].GlyphAdvance;
public int Count => _glyphInfos.Length;
public IEnumerator<double> GetEnumerator() => new ImmutableReadOnlyListStructEnumerator<double>(this);
System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() => GetEnumerator();
}
private readonly struct GlyphIndexList : IReadOnlyList<ushort>
{
private readonly ArraySlice<GlyphInfo> _glyphInfos;
public GlyphIndexList(ArraySlice<GlyphInfo> glyphInfos)
{
_glyphInfos = glyphInfos;
}
public ushort this[int index] => _glyphInfos[index].GlyphIndex;
public int Count => _glyphInfos.Length;
public IEnumerator<ushort> GetEnumerator() => new ImmutableReadOnlyListStructEnumerator<ushort>(this);
System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() => GetEnumerator();
}
private readonly struct GlyphClusterList : IReadOnlyList<int>
{
private readonly ArraySlice<GlyphInfo> _glyphInfos;
public GlyphClusterList(ArraySlice<GlyphInfo> glyphInfos)
{
_glyphInfos = glyphInfos;
}
public int this[int index] => _glyphInfos[index].GlyphCluster;
public int Count => _glyphInfos.Length;
public IEnumerator<int> GetEnumerator() => new ImmutableReadOnlyListStructEnumerator<int>(this);
System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() => GetEnumerator();
}
private readonly struct GlyphOffsetList : IReadOnlyList<Vector>
{
private readonly ArraySlice<GlyphInfo> _glyphInfos;
public GlyphOffsetList(ArraySlice<GlyphInfo> glyphInfos)
{
_glyphInfos = glyphInfos;
}
public Vector this[int index] => _glyphInfos[index].GlyphOffset;
public int Count => _glyphInfos.Length;
public IEnumerator<Vector> GetEnumerator() => new ImmutableReadOnlyListStructEnumerator<Vector>(this);
System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() => GetEnumerator();
}
}
public readonly struct GlyphInfo
{
public GlyphInfo(ushort glyphIndex, int glyphCluster, double glyphAdvance = 0, Vector glyphOffset = default)
{
GlyphIndex = glyphIndex;
GlyphAdvance = glyphAdvance;
GlyphCluster = glyphCluster;
GlyphOffset = glyphOffset;
}
/// <summary>
/// Get the glyph index.
/// </summary>
public ushort GlyphIndex { get; }
/// <summary>
/// Get the glyph cluster.
/// </summary>
public int GlyphCluster { get; }
/// <summary>
/// Get the glyph advance.
/// </summary>
public double GlyphAdvance { get; }
/// <summary>
/// Get the glyph offset.
/// </summary>
public Vector GlyphOffset { get; }
}
}

207
src/Avalonia.Visuals/Media/TextFormatting/ShapedTextCharacters.cs

@ -1,4 +1,6 @@
using Avalonia.Utilities; using System;
using Avalonia.Media.TextFormatting.Unicode;
using Avalonia.Utilities;
namespace Avalonia.Media.TextFormatting namespace Avalonia.Media.TextFormatting
{ {
@ -7,15 +9,23 @@ namespace Avalonia.Media.TextFormatting
/// </summary> /// </summary>
public sealed class ShapedTextCharacters : DrawableTextRun public sealed class ShapedTextCharacters : DrawableTextRun
{ {
public ShapedTextCharacters(GlyphRun glyphRun, TextRunProperties properties) private GlyphRun? _glyphRun;
public ShapedTextCharacters(ShapedBuffer shapedBuffer, TextRunProperties properties)
{ {
Text = glyphRun.Characters; ShapedBuffer = shapedBuffer;
Text = shapedBuffer.Text;
Properties = properties; Properties = properties;
TextSourceLength = Text.Length; TextSourceLength = Text.Length;
FontMetrics = new FontMetrics(Properties.Typeface, Properties.FontRenderingEmSize); FontMetrics = new FontMetrics(properties.Typeface, properties.FontRenderingEmSize);
GlyphRun = glyphRun;
} }
public bool IsReversed { get; private set; }
public sbyte BidiLevel => ShapedBuffer.BidiLevel;
public ShapedBuffer ShapedBuffer { get; }
/// <inheritdoc/> /// <inheritdoc/>
public override ReadOnlySlice<char> Text { get; } public override ReadOnlySlice<char> Text { get; }
@ -25,31 +35,29 @@ namespace Avalonia.Media.TextFormatting
/// <inheritdoc/> /// <inheritdoc/>
public override int TextSourceLength { get; } public override int TextSourceLength { get; }
/// <inheritdoc/> public FontMetrics FontMetrics { get; }
public override Size Size => GlyphRun.Size; public override Size Size => GlyphRun.Size;
/// <summary> public GlyphRun GlyphRun
/// Gets the font metrics. {
/// </summary> get
/// <value> {
/// The font metrics. if(_glyphRun is null)
/// </value> {
public FontMetrics FontMetrics { get; } _glyphRun = CreateGlyphRun();
}
/// <summary> return _glyphRun;
/// Gets the glyph run. }
/// </summary> }
/// <value>
/// The glyphs.
/// </value>
public GlyphRun GlyphRun { get; }
/// <inheritdoc/> /// <inheritdoc/>
public override void Draw(DrawingContext drawingContext, Point origin) public override void Draw(DrawingContext drawingContext, Point origin)
{ {
using (drawingContext.PushPreTransform(Matrix.CreateTranslation(origin))) using (drawingContext.PushPreTransform(Matrix.CreateTranslation(origin)))
{ {
if (GlyphRun.GlyphIndices.Length == 0) if (GlyphRun.GlyphIndices.Count == 0)
{ {
return; return;
} }
@ -78,116 +86,97 @@ namespace Avalonia.Media.TextFormatting
foreach (var textDecoration in Properties.TextDecorations) foreach (var textDecoration in Properties.TextDecorations)
{ {
textDecoration.Draw(drawingContext, this); textDecoration.Draw(drawingContext, GlyphRun, FontMetrics, Properties.ForegroundBrush);
} }
} }
} }
internal void Reverse()
{
_glyphRun = null;
ShapedBuffer.GlyphInfos.Span.Reverse();
IsReversed = !IsReversed;
}
/// <summary> /// <summary>
/// Splits the <see cref="TextRun"/> at specified length. /// Measures the number of characters that fit into available width.
/// </summary> /// </summary>
/// <param name="length">The length.</param> /// <param name="availableWidth">The available width.</param>
/// <returns>The split result.</returns> /// <param name="length">The count of fitting characters.</param>
public SplitTextCharactersResult Split(int length) /// <returns>
/// <c>true</c> if characters fit into the available width; otherwise, <c>false</c>.
/// </returns>
internal bool TryMeasureCharacters(double availableWidth, out int length)
{ {
var glyphCount = GlyphRun.IsLeftToRight ? length = 0;
GlyphRun.FindGlyphIndex(GlyphRun.Characters.Start + length) : var currentWidth = 0.0;
GlyphRun.FindGlyphIndex(GlyphRun.Characters.End - length);
if (GlyphRun.Characters.Length == length) for (var i = 0; i < ShapedBuffer.Length; i++)
{ {
return new SplitTextCharactersResult(this, null); var advance = ShapedBuffer.GlyphAdvances[i];
if (currentWidth + advance > availableWidth)
{
break;
}
Codepoint.ReadAt(GlyphRun.Characters, length, out var count);
length += count;
currentWidth += advance;
} }
if (GlyphRun.GlyphIndices.Length == glyphCount) return length > 0;
}
internal SplitResult<ShapedTextCharacters> Split(int length)
{
if (IsReversed)
{ {
return new SplitTextCharactersResult(this, null); Reverse();
} }
if (GlyphRun.IsLeftToRight) if(length == 0)
{ {
var firstGlyphRun = new GlyphRun( throw new ArgumentOutOfRangeException(nameof(length), "length must be greater than zero.");
Properties.Typeface.GlyphTypeface,
Properties.FontRenderingEmSize,
GlyphRun.GlyphIndices.Take(glyphCount),
GlyphRun.GlyphAdvances.Take(glyphCount),
GlyphRun.GlyphOffsets.Take(glyphCount),
GlyphRun.Characters.Take(length),
GlyphRun.GlyphClusters.Take(glyphCount),
GlyphRun.BiDiLevel);
var firstTextRun = new ShapedTextCharacters(firstGlyphRun, Properties);
var secondGlyphRun = new GlyphRun(
Properties.Typeface.GlyphTypeface,
Properties.FontRenderingEmSize,
GlyphRun.GlyphIndices.Skip(glyphCount),
GlyphRun.GlyphAdvances.Skip(glyphCount),
GlyphRun.GlyphOffsets.Skip(glyphCount),
GlyphRun.Characters.Skip(length),
GlyphRun.GlyphClusters.Skip(glyphCount),
GlyphRun.BiDiLevel);
var secondTextRun = new ShapedTextCharacters(secondGlyphRun, Properties);
return new SplitTextCharactersResult(firstTextRun, secondTextRun);
} }
else
if(length == ShapedBuffer.Length)
{ {
var take = GlyphRun.GlyphIndices.Length - glyphCount; return new SplitResult<ShapedTextCharacters>(this, null);
var firstGlyphRun = new GlyphRun(
Properties.Typeface.GlyphTypeface,
Properties.FontRenderingEmSize,
GlyphRun.GlyphIndices.Take(take),
GlyphRun.GlyphAdvances.Take(take),
GlyphRun.GlyphOffsets.Take(take),
GlyphRun.Characters.Skip(length),
GlyphRun.GlyphClusters.Take(take),
GlyphRun.BiDiLevel);
var firstTextRun = new ShapedTextCharacters(firstGlyphRun, Properties);
var secondGlyphRun = new GlyphRun(
Properties.Typeface.GlyphTypeface,
Properties.FontRenderingEmSize,
GlyphRun.GlyphIndices.Skip(take),
GlyphRun.GlyphAdvances.Skip(take),
GlyphRun.GlyphOffsets.Skip(take),
GlyphRun.Characters.Take(length),
GlyphRun.GlyphClusters.Skip(take),
GlyphRun.BiDiLevel);
var secondTextRun = new ShapedTextCharacters(secondGlyphRun, Properties);
return new SplitTextCharactersResult(secondTextRun,firstTextRun);
} }
}
public readonly struct SplitTextCharactersResult var splitBuffer = ShapedBuffer.Split(length);
{
public SplitTextCharactersResult(ShapedTextCharacters first, ShapedTextCharacters? second) var first = new ShapedTextCharacters(splitBuffer.First, Properties);
{
First = first; #if DEBUG
Second = second; if (first.Text.Length != length)
{
throw new InvalidOperationException("Split length mismatch.");
} }
#endif
var second = new ShapedTextCharacters(splitBuffer.Second!, Properties);
/// <summary> return new SplitResult<ShapedTextCharacters>(first, second);
/// Gets the first text run. }
/// </summary>
/// <value> internal GlyphRun CreateGlyphRun()
/// The first text run. {
/// </value> return new GlyphRun(
public ShapedTextCharacters First { get; } ShapedBuffer.GlyphTypeface,
ShapedBuffer.FontRenderingEmSize,
/// <summary> Text,
/// Gets the second text run. ShapedBuffer.GlyphIndices,
/// </summary> ShapedBuffer.GlyphAdvances,
/// <value> ShapedBuffer.GlyphOffsets,
/// The second text run. ShapedBuffer.GlyphClusters,
/// </value> BidiLevel);
public ShapedTextCharacters? Second { get; }
} }
} }
} }

28
src/Avalonia.Visuals/Media/TextFormatting/SplitResult.cs

@ -0,0 +1,28 @@
namespace Avalonia.Media.TextFormatting
{
internal readonly struct SplitResult<T>
{
public SplitResult(T first, T? second)
{
First = first;
Second = second;
}
/// <summary>
/// Gets the first part.
/// </summary>
/// <value>
/// The first part.
/// </value>
public T First { get; }
/// <summary>
/// Gets the second part.
/// </summary>
/// <value>
/// The second part.
/// </value>
public T? Second { get; }
}
}

134
src/Avalonia.Visuals/Media/TextFormatting/TextCharacters.cs

@ -1,4 +1,5 @@
using System.Collections.Generic; using System;
using System.Collections.Generic;
using Avalonia.Media.TextFormatting.Unicode; using Avalonia.Media.TextFormatting.Unicode;
using Avalonia.Utilities; using Avalonia.Utilities;
@ -37,19 +38,20 @@ namespace Avalonia.Media.TextFormatting
/// Gets a list of <see cref="ShapeableTextCharacters"/>. /// Gets a list of <see cref="ShapeableTextCharacters"/>.
/// </summary> /// </summary>
/// <returns>The shapeable text characters.</returns> /// <returns>The shapeable text characters.</returns>
internal IList<ShapeableTextCharacters> GetShapeableCharacters() internal IList<ShapeableTextCharacters> GetShapeableCharacters(ReadOnlySlice<char> runText, sbyte biDiLevel,
ref TextRunProperties? previousProperties)
{ {
var shapeableCharacters = new List<ShapeableTextCharacters>(2); var shapeableCharacters = new List<ShapeableTextCharacters>(2);
var runText = Text;
while (!runText.IsEmpty) while (!runText.IsEmpty)
{ {
var shapeableRun = CreateShapeableRun(runText, Properties); var shapeableRun = CreateShapeableRun(runText, Properties, biDiLevel, ref previousProperties);
shapeableCharacters.Add(shapeableRun); shapeableCharacters.Add(shapeableRun);
runText = runText.Skip(shapeableRun.Text.Length); runText = runText.Skip(shapeableRun.Text.Length);
previousProperties = shapeableRun.Properties;
} }
return shapeableCharacters; return shapeableCharacters;
@ -60,34 +62,70 @@ namespace Avalonia.Media.TextFormatting
/// </summary> /// </summary>
/// <param name="text">The text to create text runs from.</param> /// <param name="text">The text to create text runs from.</param>
/// <param name="defaultProperties">The default text run properties.</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> /// <returns>A list of shapeable text runs.</returns>
private ShapeableTextCharacters CreateShapeableRun(ReadOnlySlice<char> text, TextRunProperties defaultProperties) private static ShapeableTextCharacters CreateShapeableRun(ReadOnlySlice<char> text,
TextRunProperties defaultProperties, sbyte biDiLevel, ref TextRunProperties? previousProperties)
{ {
var defaultTypeface = defaultProperties.Typeface; var defaultTypeface = defaultProperties.Typeface;
var currentTypeface = defaultTypeface; var currentTypeface = defaultTypeface;
var previousTypeface = previousProperties?.Typeface;
if (TryGetRunProperties(text, currentTypeface, defaultTypeface, out var count)) if (TryGetShapeableLength(text, currentTypeface, out var count, out var script))
{ {
if (script == Script.Common && previousTypeface is not null)
{
if(TryGetShapeableLength(text, previousTypeface.Value, out var fallbackCount, out _))
{
return new ShapeableTextCharacters(text.Take(fallbackCount),
new GenericTextRunProperties(previousTypeface.Value, defaultProperties.FontRenderingEmSize,
defaultProperties.TextDecorations, defaultProperties.ForegroundBrush), biDiLevel);
}
}
return new ShapeableTextCharacters(text.Take(count), return new ShapeableTextCharacters(text.Take(count),
new GenericTextRunProperties(currentTypeface, defaultProperties.FontRenderingEmSize, new GenericTextRunProperties(currentTypeface, defaultProperties.FontRenderingEmSize,
defaultProperties.TextDecorations, defaultProperties.ForegroundBrush)); defaultProperties.TextDecorations, defaultProperties.ForegroundBrush), biDiLevel);
}
if (previousTypeface is not null)
{
if(TryGetShapeableLength(text, previousTypeface.Value, out count, out _))
{
return new ShapeableTextCharacters(text.Take(count),
new GenericTextRunProperties(previousTypeface.Value, defaultProperties.FontRenderingEmSize,
defaultProperties.TextDecorations, defaultProperties.ForegroundBrush), biDiLevel);
}
} }
var codepoint = Codepoint.ReadAt(text, count, out _); var codepoint = Codepoint.ReplacementCodepoint;
var codepointEnumerator = new CodepointEnumerator(text.Skip(count));
while (codepointEnumerator.MoveNext())
{
if (codepointEnumerator.Current.IsWhiteSpace)
{
continue;
}
codepoint = codepointEnumerator.Current;
break;
}
//ToDo: Fix FontFamily fallback //ToDo: Fix FontFamily fallback
var matchFound = var matchFound =
FontManager.Current.TryMatchCharacter(codepoint, defaultTypeface.Style, defaultTypeface.Weight, FontManager.Current.TryMatchCharacter(codepoint, defaultTypeface.Style, defaultTypeface.Weight,
defaultTypeface.FontFamily, defaultProperties.CultureInfo, out currentTypeface); defaultTypeface.FontFamily, defaultProperties.CultureInfo, out currentTypeface);
if (matchFound && TryGetRunProperties(text, currentTypeface, defaultTypeface, out count)) if (matchFound && TryGetShapeableLength(text, currentTypeface, out count, out _))
{ {
//Fallback found //Fallback found
return new ShapeableTextCharacters(text.Take(count), return new ShapeableTextCharacters(text.Take(count),
new GenericTextRunProperties(currentTypeface, defaultProperties.FontRenderingEmSize, new GenericTextRunProperties(currentTypeface, defaultProperties.FontRenderingEmSize,
defaultProperties.TextDecorations, defaultProperties.ForegroundBrush)); defaultProperties.TextDecorations, defaultProperties.ForegroundBrush), biDiLevel);
} }
// no fallback found // no fallback found
@ -111,35 +149,30 @@ namespace Avalonia.Media.TextFormatting
return new ShapeableTextCharacters(text.Take(count), return new ShapeableTextCharacters(text.Take(count),
new GenericTextRunProperties(currentTypeface, defaultProperties.FontRenderingEmSize, new GenericTextRunProperties(currentTypeface, defaultProperties.FontRenderingEmSize,
defaultProperties.TextDecorations, defaultProperties.ForegroundBrush)); defaultProperties.TextDecorations, defaultProperties.ForegroundBrush), biDiLevel);
} }
/// <summary> /// <summary>
/// Tries to get run properties. /// Tries to get a shapeable length that is supported by the specified typeface.
/// </summary> /// </summary>
/// <param name="defaultTypeface"></param> /// <param name="text">The text.</param>
/// <param name="text"></param>
/// <param name="typeface">The typeface that is used to find matching characters.</param> /// <param name="typeface">The typeface that is used to find matching characters.</param>
/// <param name="count"></param> /// <param name="length">The shapeable length.</param>
/// <param name="script"></param>
/// <returns></returns> /// <returns></returns>
protected bool TryGetRunProperties(ReadOnlySlice<char> text, Typeface typeface, Typeface defaultTypeface, protected static bool TryGetShapeableLength(ReadOnlySlice<char> text, Typeface typeface, out int length,
out int count) out Script script)
{ {
length = 0;
script = Script.Unknown;
if (text.Length == 0) if (text.Length == 0)
{ {
count = 0;
return false; return false;
} }
var isFallback = typeface != defaultTypeface;
count = 0;
var script = Script.Unknown;
var direction = BiDiClass.LeftToRight;
var font = typeface.GlyphTypeface; var font = typeface.GlyphTypeface;
var defaultFont = defaultTypeface.GlyphTypeface;
var enumerator = new GraphemeEnumerator(text); var enumerator = new GraphemeEnumerator(text);
while (enumerator.MoveNext()) while (enumerator.MoveNext())
@ -148,20 +181,10 @@ namespace Avalonia.Media.TextFormatting
var currentScript = currentGrapheme.FirstCodepoint.Script; var currentScript = currentGrapheme.FirstCodepoint.Script;
var currentDirection = currentGrapheme.FirstCodepoint.BiDiClass;
//// ToDo: Implement BiDi algorithm
//if (currentScript.HorizontalDirection != direction)
//{
// if (!UnicodeUtility.IsWhiteSpace(grapheme.FirstCodepoint))
// {
// break;
// }
//}
if (currentScript != script) if (currentScript != script)
{ {
if (script is Script.Unknown) if (script is Script.Unknown || currentScript != Script.Common &&
script is Script.Common or Script.Inherited)
{ {
script = currentScript; script = currentScript;
} }
@ -174,37 +197,16 @@ namespace Avalonia.Media.TextFormatting
} }
} }
//Only handle non whitespace here //Stop at the first missing glyph
if (!currentGrapheme.FirstCodepoint.IsWhiteSpace) if (!currentGrapheme.FirstCodepoint.IsBreakChar && !font.TryGetGlyph(currentGrapheme.FirstCodepoint, out _))
{
//Stop at the first glyph that is present in the default typeface.
if (isFallback && defaultFont.TryGetGlyph(currentGrapheme.FirstCodepoint, out _))
{
break;
}
//Stop at the first missing glyph
if (!font.TryGetGlyph(currentGrapheme.FirstCodepoint, out _))
{
break;
}
}
if (!currentGrapheme.FirstCodepoint.IsWhiteSpace && !font.TryGetGlyph(currentGrapheme.FirstCodepoint, out _))
{
break;
}
if (direction == BiDiClass.RightToLeft && currentDirection == BiDiClass.CommonSeparator)
{ {
break; break;
} }
count += currentGrapheme.Text.Length; length += currentGrapheme.Text.Length;
direction = currentDirection;
} }
return count > 0; return length > 0;
} }
} }
} }

9
src/Avalonia.Visuals/Media/TextFormatting/TextEndOfParagraph.cs

@ -5,5 +5,14 @@
/// </summary> /// </summary>
public class TextEndOfParagraph : TextEndOfLine public class TextEndOfParagraph : TextEndOfLine
{ {
public TextEndOfParagraph()
{
}
public TextEndOfParagraph(int textSourceLength) : base(textSourceLength)
{
}
} }
} }

669
src/Avalonia.Visuals/Media/TextFormatting/TextFormatterImpl.cs

@ -1,6 +1,8 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using Avalonia.Media.TextFormatting.Unicode; using Avalonia.Media.TextFormatting.Unicode;
using Avalonia.Utilities;
namespace Avalonia.Media.TextFormatting namespace Avalonia.Media.TextFormatting
{ {
@ -11,172 +13,56 @@ namespace Avalonia.Media.TextFormatting
TextParagraphProperties paragraphProperties, TextLineBreak? previousLineBreak = null) TextParagraphProperties paragraphProperties, TextLineBreak? previousLineBreak = null)
{ {
var textWrapping = paragraphProperties.TextWrapping; var textWrapping = paragraphProperties.TextWrapping;
FlowDirection flowDirection;
TextLineBreak? nextLineBreak = null;
List<ShapedTextCharacters> shapedRuns;
var textRuns = FetchTextRuns(textSource, firstTextSourceIndex, previousLineBreak, var textRuns = FetchTextRuns(textSource, firstTextSourceIndex,
out var nextLineBreak); out var textEndOfLine, out var textRange);
var textRange = GetTextRange(textRuns); if (previousLineBreak?.RemainingCharacters != null)
TextLine textLine;
switch (textWrapping)
{
case TextWrapping.NoWrap:
{
textLine = new TextLineImpl(textRuns, textRange, paragraphWidth, paragraphProperties,
nextLineBreak);
break;
}
case TextWrapping.WrapWithOverflow:
case TextWrapping.Wrap:
{
textLine = PerformTextWrapping(textRuns, textRange, paragraphWidth, paragraphProperties,
nextLineBreak);
break;
}
default:
throw new ArgumentOutOfRangeException();
}
return textLine;
}
/// <summary>
/// Measures the number of characters that fit into available width.
/// </summary>
/// <param name="textCharacters">The text run.</param>
/// <param name="availableWidth">The available width.</param>
/// <param name="count">The count of fitting characters.</param>
/// <returns>
/// <c>true</c> if characters fit into the available width; otherwise, <c>false</c>.
/// </returns>
internal static bool TryMeasureCharacters(ShapedTextCharacters textCharacters, double availableWidth,
out int count)
{
var glyphRun = textCharacters.GlyphRun;
if (glyphRun.Size.Width < availableWidth)
{ {
count = glyphRun.Characters.Length; flowDirection = previousLineBreak.FlowDirection;
shapedRuns = previousLineBreak.RemainingCharacters.ToList();
return true; nextLineBreak = previousLineBreak;
} }
else
var glyphCount = 0;
var currentWidth = 0.0;
if (glyphRun.GlyphAdvances.IsEmpty)
{ {
var glyphTypeface = glyphRun.GlyphTypeface; shapedRuns = ShapeTextRuns(textRuns, paragraphProperties.FlowDirection,out flowDirection);
if (glyphRun.IsLeftToRight) if(nextLineBreak == null && textEndOfLine != null)
{ {
foreach (var glyph in glyphRun.GlyphIndices) nextLineBreak = new TextLineBreak(textEndOfLine, flowDirection);
{
var advance = glyphTypeface.GetGlyphAdvance(glyph) * glyphRun.Scale;
if (currentWidth + advance > availableWidth)
{
break;
}
currentWidth += advance;
glyphCount++;
}
} }
else }
{
for (var index = glyphRun.GlyphClusters.Length - 1; index > 0; index--)
{
var glyph = glyphRun.GlyphIndices[index];
var advance = glyphTypeface.GetGlyphAdvance(glyph) * glyphRun.Scale;
if (currentWidth + advance > availableWidth)
{
break;
}
currentWidth += advance; TextLineImpl textLine;
glyphCount++; switch (textWrapping)
}
}
}
else
{ {
if (glyphRun.IsLeftToRight) case TextWrapping.NoWrap:
{
for (var index = 0; index < glyphRun.GlyphAdvances.Length; index++)
{ {
var advance = glyphRun.GlyphAdvances[index]; TextLineImpl.SortRuns(shapedRuns);
if (currentWidth + advance > availableWidth)
{
break;
}
currentWidth += advance; textLine = new TextLineImpl(shapedRuns, textRange, paragraphWidth, paragraphProperties,
flowDirection, nextLineBreak);
glyphCount++; textLine.FinalizeLine();
break;
} }
} case TextWrapping.WrapWithOverflow:
else case TextWrapping.Wrap:
{
for (var index = glyphRun.GlyphAdvances.Length - 1; index > 0; index--)
{ {
var advance = glyphRun.GlyphAdvances[index]; textLine = PerformTextWrapping(shapedRuns, textRange, paragraphWidth, paragraphProperties,
flowDirection, nextLineBreak);
if (currentWidth + advance > availableWidth) break;
{
break;
}
currentWidth += advance;
glyphCount++;
} }
} default:
} throw new ArgumentOutOfRangeException(nameof(textWrapping));
if (glyphCount == 0)
{
count = 0;
return false;
}
if (glyphCount == glyphRun.GlyphIndices.Length)
{
count = glyphRun.Characters.Length;
return true;
}
if (glyphRun.GlyphClusters.IsEmpty)
{
count = glyphCount;
return true;
}
var firstCluster = glyphRun.GlyphClusters[0];
var lastCluster = glyphRun.GlyphClusters[glyphCount];
if (glyphRun.IsLeftToRight)
{
count = lastCluster - firstCluster;
}
else
{
count = firstCluster - lastCluster;
} }
return count > 0; return textLine;
} }
/// <summary> /// <summary>
@ -185,7 +71,7 @@ namespace Avalonia.Media.TextFormatting
/// <param name="textRuns">The text run's.</param> /// <param name="textRuns">The text run's.</param>
/// <param name="length">The length to split at.</param> /// <param name="length">The length to split at.</param>
/// <returns>The split text runs.</returns> /// <returns>The split text runs.</returns>
internal static SplitTextRunsResult SplitTextRuns(List<ShapedTextCharacters> textRuns, int length) internal static SplitResult<List<ShapedTextCharacters>> SplitShapedRuns(List<ShapedTextCharacters> textRuns, int length)
{ {
var currentLength = 0; var currentLength = 0;
@ -193,13 +79,13 @@ namespace Avalonia.Media.TextFormatting
{ {
var currentRun = textRuns[i]; var currentRun = textRuns[i];
if (currentLength + currentRun.GlyphRun.Characters.Length <= length) if (currentLength + currentRun.Text.Length < length)
{ {
currentLength += currentRun.GlyphRun.Characters.Length; currentLength += currentRun.Text.Length;
continue; continue;
} }
var firstCount = currentRun.GlyphRun.Characters.Length >= 1 ? i + 1 : i; var firstCount = currentRun.Text.Length >= 1 ? i + 1 : i;
var first = new List<ShapedTextCharacters>(firstCount); var first = new List<ShapedTextCharacters>(firstCount);
@ -213,14 +99,14 @@ namespace Avalonia.Media.TextFormatting
var secondCount = textRuns.Count - firstCount; var secondCount = textRuns.Count - firstCount;
if (currentLength + currentRun.GlyphRun.Characters.Length == length) if (currentLength + currentRun.Text.Length == length)
{ {
var second = new List<ShapedTextCharacters>(secondCount); var second = secondCount > 0 ? new List<ShapedTextCharacters>(secondCount) : null;
var offset = currentRun.GlyphRun.Characters.Length > 1 ? 1 : 0;
if (secondCount > 0) if (second != null)
{ {
var offset = currentRun.Text.Length >= 1 ? 1 : 0;
for (var j = 0; j < secondCount; j++) for (var j = 0; j < secondCount; j++)
{ {
second.Add(textRuns[i + j + offset]); second.Add(textRuns[i + j + offset]);
@ -229,7 +115,7 @@ namespace Avalonia.Media.TextFormatting
first.Add(currentRun); first.Add(currentRun);
return new SplitTextRunsResult(first, second); return new SplitResult<List<ShapedTextCharacters>>(first, second);
} }
else else
{ {
@ -243,120 +129,203 @@ namespace Avalonia.Media.TextFormatting
second.Add(split.Second!); second.Add(split.Second!);
if (secondCount > 0) for (var j = 1; j < secondCount; j++)
{ {
for (var j = 1; j < secondCount; j++) second.Add(textRuns[i + j]);
{
second.Add(textRuns[i + j]);
}
} }
return new SplitTextRunsResult(first, second); return new SplitResult<List<ShapedTextCharacters>>(first, second);
} }
} }
return new SplitTextRunsResult(textRuns, null); return new SplitResult<List<ShapedTextCharacters>>(textRuns, null);
} }
/// <summary> /// <summary>
/// Fetches text runs. /// Shape specified text runs with specified paragraph embedding.
/// </summary> /// </summary>
/// <param name="textSource">The text source.</param> /// <param name="textRuns">The text runs to shape.</param>
/// <param name="firstTextSourceIndex">The first text source index.</param> /// <param name="flowDirection">The paragraph embedding level.</param>
/// <param name="previousLineBreak">Previous line break. Can be null.</param> /// <param name="resolvedFlowDirection">The resolved flow direction.</param>
/// <param name="nextLineBreak">Next line break. Can be null.</param>
/// <returns> /// <returns>
/// The formatted text runs. /// A list of shaped text characters.
/// </returns> /// </returns>
private static List<ShapedTextCharacters> FetchTextRuns(ITextSource textSource, private static List<ShapedTextCharacters> ShapeTextRuns(List<TextCharacters> textRuns,
int firstTextSourceIndex, TextLineBreak? previousLineBreak, out TextLineBreak? nextLineBreak) FlowDirection flowDirection, out FlowDirection resolvedFlowDirection)
{ {
nextLineBreak = default; var shapedTextCharacters = new List<ShapedTextCharacters>();
var currentLength = 0; var biDiData = new BidiData((sbyte)flowDirection);
var textRuns = new List<ShapedTextCharacters>(); foreach (var textRun in textRuns)
{
biDiData.Append(textRun.Text);
}
if (previousLineBreak?.RemainingCharacters != null) var biDi = BidiAlgorithm.Instance.Value!;
biDi.Process(biDiData);
var resolvedEmbeddingLevel = biDi.ResolveEmbeddingLevel(biDiData.Classes);
resolvedFlowDirection =
(resolvedEmbeddingLevel & 1) == 0 ? FlowDirection.LeftToRight : FlowDirection.RightToLeft;
foreach (var shapeableRuns in CoalesceLevels(textRuns, biDi.ResolvedLevels))
{ {
for (var index = 0; index < previousLineBreak.RemainingCharacters.Count; index++) for (var index = 0; index < shapeableRuns.Count; index++)
{ {
var shapedCharacters = previousLineBreak.RemainingCharacters[index]; var currentRun = shapeableRuns[index];
var shapedBuffer = TextShaper.Current.ShapeText(currentRun.Text, currentRun.Properties.Typeface.GlyphTypeface,
currentRun.Properties.FontRenderingEmSize, currentRun.Properties.CultureInfo, currentRun.BidiLevel);
var shapedCharacters = new ShapedTextCharacters(shapedBuffer, currentRun.Properties);
shapedTextCharacters.Add(shapedCharacters);
}
}
return shapedTextCharacters;
}
/// <summary>
/// Coalesces ranges of the same bidi level to form <see cref="ShapeableTextCharacters"/>
/// </summary>
/// <param name="textCharacters">The text characters to form <see cref="ShapeableTextCharacters"/> from.</param>
/// <param name="levels">The bidi levels.</param>
/// <returns></returns>
private static IEnumerable<IList<ShapeableTextCharacters>> CoalesceLevels(
IReadOnlyList<TextCharacters> textCharacters,
ReadOnlySlice<sbyte> levels)
{
if (levels.Length == 0)
{
yield break;
}
var levelIndex = 0;
var runLevel = levels[0];
textRuns.Add(shapedCharacters); TextRunProperties? previousProperties = null;
TextCharacters? currentRun = null;
var runText = ReadOnlySlice<char>.Empty;
if (TryGetLineBreak(shapedCharacters, out var runLineBreak)) for (var i = 0; i < textCharacters.Count; i++)
{
var j = 0;
currentRun = textCharacters[i];
runText = currentRun.Text;
for (; j < runText.Length;)
{
Codepoint.ReadAt(runText, j, out var count);
if (levelIndex + 1 == levels.Length)
{ {
var splitResult = SplitTextRuns(textRuns, currentLength + runLineBreak.PositionWrap); break;
}
if (splitResult.Second == null) levelIndex++;
{ j += count;
return splitResult.First;
}
if (++index < previousLineBreak.RemainingCharacters.Count) if (j == runText.Length)
{ {
for (; index < previousLineBreak.RemainingCharacters.Count; index++) yield return currentRun.GetShapeableCharacters(runText.Take(j), runLevel, ref previousProperties);
{
splitResult.Second.Add(previousLineBreak.RemainingCharacters[index]);
}
}
nextLineBreak = new TextLineBreak(splitResult.Second); runLevel = levels[levelIndex];
return splitResult.First; continue;
}
if (levels[levelIndex] == runLevel)
{
continue;
} }
currentLength += shapedCharacters.Text.Length; // End of this run
yield return currentRun.GetShapeableCharacters(runText.Take(j), runLevel, ref previousProperties);
runText = runText.Skip(j);
j = 0;
// Move to next run
runLevel = levels[levelIndex];
} }
} }
firstTextSourceIndex += currentLength; if (currentRun is null || runText.IsEmpty)
{
yield break;
}
yield return currentRun.GetShapeableCharacters(runText, runLevel, ref previousProperties);
}
/// <summary>
/// Fetches text runs.
/// </summary>
/// <param name="textSource">The text source.</param>
/// <param name="firstTextSourceIndex">The first text source index.</param>
/// <param name="endOfLine"></param>
/// <param name="textRange"></param>
/// <returns>
/// The formatted text runs.
/// </returns>
private static List<TextCharacters> FetchTextRuns(ITextSource textSource, int firstTextSourceIndex,
out TextEndOfLine? endOfLine, out TextRange textRange)
{
var length = 0;
endOfLine = null;
var textRuns = new List<TextCharacters>();
var textRunEnumerator = new TextRunEnumerator(textSource, firstTextSourceIndex); var textRunEnumerator = new TextRunEnumerator(textSource, firstTextSourceIndex);
while (textRunEnumerator.MoveNext()) while (textRunEnumerator.MoveNext())
{ {
var textRun = textRunEnumerator.Current!; var textRun = textRunEnumerator.Current;
if(textRun == null)
{
break;
}
switch (textRun) switch (textRun)
{ {
case TextCharacters textCharacters: case TextCharacters textCharacters:
{
var shapeableRuns = textCharacters.GetShapeableCharacters();
foreach (var run in shapeableRuns)
{ {
var glyphRun = TextShaper.Current.ShapeText(run.Text, run.Properties.Typeface, if (TryGetLineBreak(textCharacters, out var runLineBreak))
run.Properties.FontRenderingEmSize, run.Properties.CultureInfo); {
var splitResult = new TextCharacters(textCharacters.Text.Take(runLineBreak.PositionWrap),
textCharacters.Properties);
var shapedCharacters = new ShapedTextCharacters(glyphRun, run.Properties); textRuns.Add(splitResult);
textRuns.Add(shapedCharacters); length += runLineBreak.PositionWrap;
}
break; textRange = new TextRange(firstTextSourceIndex, length);
}
case TextEndOfLine textEndOfLine:
nextLineBreak = new TextLineBreak(textEndOfLine);
break;
}
if (TryGetLineBreak(textRun, out var runLineBreak)) return textRuns;
{ }
var splitResult = SplitTextRuns(textRuns, currentLength + runLineBreak.PositionWrap);
if (splitResult.Second != null) textRuns.Add(textCharacters);
{
nextLineBreak = new TextLineBreak(splitResult.Second);
}
return splitResult.First; break;
}
case TextEndOfLine textEndOfLine:
endOfLine = textEndOfLine;
break;
} }
currentLength += textRun.Text.Length; length += textRun.Text.Length;
} }
textRange = new TextRange(firstTextSourceIndex, length);
return textRuns; return textRuns;
} }
@ -380,49 +349,52 @@ namespace Avalonia.Media.TextFormatting
lineBreak = lineBreakEnumerator.Current; lineBreak = lineBreakEnumerator.Current;
if (lineBreak.PositionWrap >= textRun.Text.Length) return lineBreak.PositionWrap >= textRun.Text.Length || true;
{
return true;
}
return true;
} }
return false; return false;
} }
/// <summary> private static int MeasureLength(IReadOnlyList<ShapedTextCharacters> textRuns, TextRange textRange,
/// Performs text wrapping returns a list of text lines. double paragraphWidth)
/// </summary>
/// <param name="textRuns">The text run's.</param>
/// <param name="textRange">The text range that is covered by the text runs.</param>
/// <param name="paragraphWidth">The paragraph width.</param>
/// <param name="paragraphProperties">The text paragraph properties.</param>
/// <param name="currentLineBreak">The current line break if the line was explicitly broken.</param>
/// <returns>The wrapped text line.</returns>
private static TextLine PerformTextWrapping(List<ShapedTextCharacters> textRuns, TextRange textRange,
double paragraphWidth, TextParagraphProperties paragraphProperties, TextLineBreak? currentLineBreak)
{ {
var availableWidth = paragraphWidth;
var currentWidth = 0.0; var currentWidth = 0.0;
var measuredLength = 0; var lastCluster = textRange.Start;
foreach (var currentRun in textRuns) foreach (var currentRun in textRuns)
{ {
if (currentWidth + currentRun.Size.Width > availableWidth) for (var i = 0; i < currentRun.ShapedBuffer.Length; i++)
{ {
if (TryMeasureCharacters(currentRun, paragraphWidth - currentWidth, out var count)) var glyphInfo = currentRun.ShapedBuffer[i];
if (currentWidth + glyphInfo.GlyphAdvance > paragraphWidth)
{ {
measuredLength += count; return lastCluster - textRange.Start;
} }
break; lastCluster = glyphInfo.GlyphCluster;
currentWidth += glyphInfo.GlyphAdvance;
} }
}
currentWidth += currentRun.Size.Width; return textRange.Length;
}
measuredLength += currentRun.Text.Length; /// <summary>
} /// Performs text wrapping returns a list of text lines.
/// </summary>
/// <param name="textRuns"></param>
/// <param name="textRange">The text range that is covered by the text runs.</param>
/// <param name="paragraphWidth">The paragraph width.</param>
/// <param name="paragraphProperties">The text paragraph properties.</param>
/// <param name="flowDirection"></param>
/// <param name="currentLineBreak">The current line break if the line was explicitly broken.</param>
/// <returns>The wrapped text line.</returns>
private static TextLineImpl PerformTextWrapping(List<ShapedTextCharacters> textRuns, TextRange textRange,
double paragraphWidth, TextParagraphProperties paragraphProperties, FlowDirection flowDirection,
TextLineBreak? currentLineBreak)
{
var measuredLength = MeasureLength(textRuns, textRange, paragraphWidth);
var currentLength = 0; var currentLength = 0;
@ -430,154 +402,136 @@ namespace Avalonia.Media.TextFormatting
var currentPosition = 0; var currentPosition = 0;
if (measuredLength == 0 && paragraphProperties.TextWrapping != TextWrapping.WrapWithOverflow) for (var index = 0; index < textRuns.Count; index++)
{ {
measuredLength = 1; var currentRun = textRuns[index];
}
else
{
for (var index = 0; index < textRuns.Count; index++)
{
var currentRun = textRuns[index];
var lineBreaker = new LineBreakEnumerator(currentRun.Text); var lineBreaker = new LineBreakEnumerator(currentRun.Text);
var breakFound = false; var breakFound = false;
while (lineBreaker.MoveNext()) while (lineBreaker.MoveNext())
{
if (lineBreaker.Current.Required &&
currentLength + lineBreaker.Current.PositionMeasure <= measuredLength)
{ {
if (lineBreaker.Current.Required && //Explicit break found
currentLength + lineBreaker.Current.PositionMeasure <= measuredLength) breakFound = true;
{
breakFound = true;
currentPosition = currentLength + lineBreaker.Current.PositionWrap; currentPosition = currentLength + lineBreaker.Current.PositionWrap;
break; break;
} }
if ((paragraphProperties.TextWrapping != TextWrapping.WrapWithOverflow || lastWrapPosition != 0) && if (currentLength + lineBreaker.Current.PositionMeasure > measuredLength)
currentLength + lineBreaker.Current.PositionMeasure > measuredLength) {
if (paragraphProperties.TextWrapping == TextWrapping.WrapWithOverflow)
{ {
if (lastWrapPosition > 0) if (lastWrapPosition > 0)
{ {
currentPosition = lastWrapPosition; currentPosition = lastWrapPosition;
}
else
{
currentPosition = currentLength + measuredLength;
}
breakFound = true; breakFound = true;
break; break;
} }
if (currentLength + lineBreaker.Current.PositionWrap >= measuredLength)
{
currentPosition = currentLength + lineBreaker.Current.PositionWrap;
if (index < textRuns.Count - 1 && //Find next possible wrap position (overflow)
lineBreaker.Current.PositionWrap == currentRun.Text.Length) if (index < textRuns.Count - 1)
{ {
var nextRun = textRuns[index + 1]; if (lineBreaker.Current.PositionWrap != currentRun.Text.Length)
{
//We already found the next possible wrap position.
breakFound = true;
lineBreaker = new LineBreakEnumerator(nextRun.Text); currentPosition = currentLength + lineBreaker.Current.PositionWrap;
break;
}
if (lineBreaker.MoveNext() && while (lineBreaker.MoveNext() && index < textRuns.Count)
lineBreaker.Current.PositionMeasure == 0)
{ {
currentPosition += lineBreaker.Current.PositionWrap; currentPosition += lineBreaker.Current.PositionWrap;
if (lineBreaker.Current.PositionWrap != currentRun.Text.Length)
{
break;
}
index++;
if (index >= textRuns.Count)
{
break;
}
currentRun = textRuns[index];
lineBreaker = new LineBreakEnumerator(currentRun.Text);
} }
} }
else
{
currentPosition = currentLength + lineBreaker.Current.PositionWrap;
}
breakFound = true; breakFound = true;
break; break;
} }
lastWrapPosition = currentLength + lineBreaker.Current.PositionWrap; //We overflowed so we use the last available wrap position.
} currentPosition = lastWrapPosition == 0 ? measuredLength : lastWrapPosition;
if (!breakFound) breakFound = true;
{
currentLength += currentRun.Text.Length;
continue; break;
} }
measuredLength = currentPosition; if (lineBreaker.Current.PositionMeasure != lineBreaker.Current.PositionWrap)
{
break; lastWrapPosition = currentLength + lineBreaker.Current.PositionWrap;
}
} }
}
var splitResult = SplitTextRuns(textRuns, measuredLength);
textRange = new TextRange(textRange.Start, measuredLength); if (!breakFound)
{
currentLength += currentRun.Text.Length;
var remainingCharacters = splitResult.Second; continue;
}
var lineBreak = remainingCharacters?.Count > 0 ? new TextLineBreak(remainingCharacters) : null; measuredLength = currentPosition;
if (lineBreak is null && currentLineBreak?.TextEndOfLine != null) break;
{
lineBreak = new TextLineBreak(currentLineBreak.TextEndOfLine);
} }
return new TextLineImpl(splitResult.First, textRange, paragraphWidth, paragraphProperties, lineBreak); if (measuredLength == 0)
}
/// <summary>
/// Gets the text range that is covered by the text runs.
/// </summary>
/// <param name="textRuns">The text runs.</param>
/// <returns>The text range that is covered by the text runs.</returns>
private static TextRange GetTextRange(IReadOnlyList<TextRun> textRuns)
{
if (textRuns.Count == 0)
{ {
return new TextRange(); measuredLength = 1;
} }
var firstTextRun = textRuns[0]; var splitResult = SplitShapedRuns(textRuns, measuredLength);
if (textRuns.Count == 1) textRange = new TextRange(textRange.Start, measuredLength);
{
return new TextRange(firstTextRun.Text.Start, firstTextRun.Text.Length);
}
var start = firstTextRun.Text.Start; var remainingCharacters = splitResult.Second;
var end = textRuns[textRuns.Count - 1].Text.End + 1; var lineBreak = remainingCharacters?.Count > 0 ?
new TextLineBreak(currentLineBreak?.TextEndOfLine, flowDirection, remainingCharacters) :
null;
return new TextRange(start, end - start); if (lineBreak is null && currentLineBreak?.TextEndOfLine != null)
}
internal readonly struct SplitTextRunsResult
{
public SplitTextRunsResult(List<ShapedTextCharacters> first, List<ShapedTextCharacters>? second)
{ {
First = first; lineBreak = new TextLineBreak(currentLineBreak.TextEndOfLine, flowDirection);
Second = second;
} }
/// <summary> TextLineImpl.SortRuns(splitResult.First);
/// Gets the first text runs.
/// </summary> var textLine = new TextLineImpl(splitResult.First, textRange, paragraphWidth, paragraphProperties, flowDirection,
/// <value> lineBreak);
/// The first text runs.
/// </value> return textLine.FinalizeLine();
public List<ShapedTextCharacters> First { get; }
/// <summary>
/// Gets the second text runs.
/// </summary>
/// <value>
/// The second text runs.
/// </value>
public List<ShapedTextCharacters>? Second { get; }
} }
private struct TextRunEnumerator private struct TextRunEnumerator
@ -614,5 +568,28 @@ namespace Avalonia.Media.TextFormatting
return true; return true;
} }
} }
/// <summary>
/// Creates a shaped symbol.
/// </summary>
/// <param name="textRun">The symbol run to shape.</param>
/// <param name="flowDirection">The flow direction.</param>
/// <returns>
/// The shaped symbol.
/// </returns>
internal static ShapedTextCharacters CreateSymbol(TextRun textRun, FlowDirection flowDirection)
{
var textShaper = TextShaper.Current;
var glyphTypeface = textRun.Properties!.Typeface.GlyphTypeface;
var fontRenderingEmSize = textRun.Properties.FontRenderingEmSize;
var cultureInfo = textRun.Properties.CultureInfo;
var shapedBuffer = textShaper.ShapeText(textRun.Text, glyphTypeface, fontRenderingEmSize, cultureInfo, (sbyte)flowDirection);
return new ShapedTextCharacters(shapedBuffer, textRun.Properties);
}
} }
} }

567
src/Avalonia.Visuals/Media/TextFormatting/TextLayout.cs

@ -1,6 +1,5 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq; using System.Linq;
using Avalonia.Utilities; using Avalonia.Utilities;
@ -11,7 +10,7 @@ namespace Avalonia.Media.TextFormatting
/// </summary> /// </summary>
public class TextLayout public class TextLayout
{ {
private static readonly char[] s_empty = { '\u200B' }; private static readonly char[] s_empty = { ' ' };
private readonly ReadOnlySlice<char> _text; private readonly ReadOnlySlice<char> _text;
private readonly TextParagraphProperties _paragraphProperties; private readonly TextParagraphProperties _paragraphProperties;
@ -29,6 +28,7 @@ namespace Avalonia.Media.TextFormatting
/// <param name="textWrapping">The text wrapping.</param> /// <param name="textWrapping">The text wrapping.</param>
/// <param name="textTrimming">The text trimming.</param> /// <param name="textTrimming">The text trimming.</param>
/// <param name="textDecorations">The text decorations.</param> /// <param name="textDecorations">The text decorations.</param>
/// <param name="flowDirection">The text flow direction.</param>
/// <param name="maxWidth">The maximum width.</param> /// <param name="maxWidth">The maximum width.</param>
/// <param name="maxHeight">The maximum height.</param> /// <param name="maxHeight">The maximum height.</param>
/// <param name="lineHeight">The height of each line of text.</param> /// <param name="lineHeight">The height of each line of text.</param>
@ -43,6 +43,7 @@ namespace Avalonia.Media.TextFormatting
TextWrapping textWrapping = TextWrapping.NoWrap, TextWrapping textWrapping = TextWrapping.NoWrap,
TextTrimming textTrimming = TextTrimming.None, TextTrimming textTrimming = TextTrimming.None,
TextDecorationCollection? textDecorations = null, TextDecorationCollection? textDecorations = null,
FlowDirection flowDirection = FlowDirection.LeftToRight,
double maxWidth = double.PositiveInfinity, double maxWidth = double.PositiveInfinity,
double maxHeight = double.PositiveInfinity, double maxHeight = double.PositiveInfinity,
double lineHeight = double.NaN, double lineHeight = double.NaN,
@ -55,7 +56,7 @@ namespace Avalonia.Media.TextFormatting
_paragraphProperties = _paragraphProperties =
CreateTextParagraphProperties(typeface, fontSize, foreground, textAlignment, textWrapping, CreateTextParagraphProperties(typeface, fontSize, foreground, textAlignment, textWrapping,
textDecorations, lineHeight); textDecorations, flowDirection, lineHeight);
_textTrimming = textTrimming; _textTrimming = textTrimming;
@ -69,7 +70,7 @@ namespace Avalonia.Media.TextFormatting
MaxLines = maxLines; MaxLines = maxLines;
UpdateLayout(); TextLines = CreateTextLines();
} }
/// <summary> /// <summary>
@ -190,29 +191,174 @@ namespace Avalonia.Media.TextFormatting
} }
var result = new List<Rect>(TextLines.Count); var result = new List<Rect>(TextLines.Count);
var currentY = 0d; var currentY = 0d;
var currentPosition = 0;
foreach (var textLine in TextLines) foreach (var textLine in TextLines)
{ {
var currentX = textLine.Start; //Current line isn't covered.
if (currentPosition + textLine.TextRange.Length <= start)
if (textLine.TextRange.End < start)
{ {
currentY += textLine.Height; currentY += textLine.Height;
currentPosition += textLine.TextRange.Length;
continue; continue;
} }
if (start > textLine.TextRange.Start) //The whole line is covered.
if (currentPosition >= start && start + length > currentPosition + textLine.TextRange.Length)
{ {
currentX += textLine.GetDistanceFromCharacterHit(new CharacterHit(start)); result.Add(new Rect(textLine.Start, currentY, textLine.WidthIncludingTrailingWhitespace, textLine.Height));
currentY += textLine.Height;
currentPosition += textLine.TextRange.Length;
continue;
} }
var startX = textLine.Start;
//A portion of the line is covered.
for (var index = 0; index < textLine.TextRuns.Count; index++)
{
var currentRun = (ShapedTextCharacters)textLine.TextRuns[index];
ShapedTextCharacters? nextRun = null;
if (index + 1 < textLine.TextRuns.Count)
{
nextRun = (ShapedTextCharacters)textLine.TextRuns[index + 1];
}
if (nextRun != null)
{
if (nextRun.Text.Start < currentRun.Text.Start && start + length < currentRun.Text.End)
{
goto skip;
}
if (currentRun.Text.Start >= start + length)
{
goto skip;
}
if (currentRun.Text.Start > nextRun.Text.Start && currentRun.Text.Start < start)
{
goto skip;
}
if (currentRun.Text.End < start)
{
goto skip;
}
goto noop;
skip:
{
startX += currentRun.Size.Width;
currentPosition = currentRun.Text.Start;
}
continue;
noop:{ }
}
var endOffset = currentRun.GlyphRun.GetDistanceFromCharacterHit(
currentRun.ShapedBuffer.IsLeftToRight ?
new CharacterHit(start + length) :
new CharacterHit(start));
var endX = startX + endOffset;
var startOffset = currentRun.GlyphRun.GetDistanceFromCharacterHit(
currentRun.ShapedBuffer.IsLeftToRight ?
new CharacterHit(start) :
new CharacterHit(start + length));
startX += startOffset;
var characterHit = currentRun.GlyphRun.IsLeftToRight ?
currentRun.GlyphRun.GetCharacterHitFromDistance(endOffset, out _) :
currentRun.GlyphRun.GetCharacterHitFromDistance(startOffset, out _);
currentPosition = characterHit.FirstCharacterIndex + characterHit.TrailingLength;
if(nextRun != null)
{
if (currentRun.ShapedBuffer.IsLeftToRight == nextRun.ShapedBuffer.IsLeftToRight)
{
endOffset = nextRun.GlyphRun.GetDistanceFromCharacterHit(
nextRun.ShapedBuffer.IsLeftToRight ?
new CharacterHit(start + length) :
new CharacterHit(start));
index++;
endX += endOffset;
currentRun = nextRun;
if (currentRun.ShapedBuffer.IsLeftToRight)
{
characterHit = nextRun.GlyphRun.GetCharacterHitFromDistance(endOffset, out _);
currentPosition = characterHit.FirstCharacterIndex + characterHit.TrailingLength;
}
}
}
var endX = textLine.GetDistanceFromCharacterHit(new CharacterHit(start + length)); if (endX < startX)
{
(endX, startX) = (startX, endX);
}
var width = endX - startX;
result.Add(new Rect(currentX, currentY, endX - currentX, textLine.Height)); result.Add(new Rect(startX, currentY, width, textLine.Height));
if (currentRun.ShapedBuffer.IsLeftToRight)
{
if (nextRun != null)
{
if (nextRun.Text.Start > currentRun.Text.Start)
{
break;
}
currentPosition = nextRun.Text.End;
}
else
{
if (currentPosition >= start + length)
{
break;
}
}
}
else
{
if (currentPosition <= start)
{
break;
}
}
if (!currentRun.ShapedBuffer.IsLeftToRight && currentPosition != currentRun.Text.Start)
{
endX += currentRun.GlyphRun.Size.Width - endOffset;
}
startX = endX;
}
if (currentPosition == start || currentPosition == start + length)
{
break;
}
if (textLine.TextRange.Start + textLine.TextRange.Length >= start + length) if (textLine.TextRange.Start + textLine.TextRange.Length >= start + length)
{ {
break; break;
@ -256,6 +402,37 @@ namespace Avalonia.Media.TextFormatting
return GetHitTestResult(currentLine, characterHit, point); return GetHitTestResult(currentLine, characterHit, point);
} }
public int GetLineIndexFromCharacterIndex(int charIndex, bool trailingEdge)
{
if (charIndex < 0)
{
return 0;
}
if (charIndex > _text.Length)
{
return TextLines.Count - 1;
}
for (var index = 0; index < TextLines.Count; index++)
{
var textLine = TextLines[index];
if (textLine.TextRange.Start + textLine.TextRange.Length < charIndex)
{
continue;
}
if (charIndex >= textLine.Start && charIndex <= textLine.TextRange.End + (trailingEdge ? 1 : 0))
{
return index;
}
}
return TextLines.Count - 1;
}
private TextHitTestResult GetHitTestResult(TextLine textLine, CharacterHit characterHit, Point point) private TextHitTestResult GetHitTestResult(TextLine textLine, CharacterHit characterHit, Point point)
{ {
var (x, y) = point; var (x, y) = point;
@ -274,7 +451,18 @@ namespace Avalonia.Media.TextFormatting
var isTrailing = lastTrailingIndex == textPosition && characterHit.TrailingLength > 0 || var isTrailing = lastTrailingIndex == textPosition && characterHit.TrailingLength > 0 ||
y > Size.Height; y > Size.Height;
return new TextHitTestResult { IsInside = isInside, IsTrailing = isTrailing, TextPosition = textPosition }; if (textPosition == textLine.TextRange.Start + textLine.TextRange.Length)
{
textPosition -= textLine.NewLineLength;
}
if (textLine.NewLineLength > 0 && textPosition + textLine.NewLineLength ==
characterHit.FirstCharacterIndex + characterHit.TrailingLength)
{
characterHit = new CharacterHit(characterHit.FirstCharacterIndex);
}
return new TextHitTestResult(characterHit, textPosition, isInside, isTrailing);
} }
/// <summary> /// <summary>
@ -286,15 +474,16 @@ namespace Avalonia.Media.TextFormatting
/// <param name="textAlignment">The text alignment.</param> /// <param name="textAlignment">The text alignment.</param>
/// <param name="textWrapping">The text wrapping.</param> /// <param name="textWrapping">The text wrapping.</param>
/// <param name="textDecorations">The text decorations.</param> /// <param name="textDecorations">The text decorations.</param>
/// <param name="flowDirection">The text flow direction.</param>
/// <param name="lineHeight">The height of each line of text.</param> /// <param name="lineHeight">The height of each line of text.</param>
/// <returns></returns> /// <returns></returns>
private static TextParagraphProperties CreateTextParagraphProperties(Typeface typeface, double fontSize, private static TextParagraphProperties CreateTextParagraphProperties(Typeface typeface, double fontSize,
IBrush foreground, TextAlignment textAlignment, TextWrapping textWrapping, IBrush foreground, TextAlignment textAlignment, TextWrapping textWrapping,
TextDecorationCollection? textDecorations, double lineHeight) TextDecorationCollection? textDecorations, FlowDirection flowDirection, double lineHeight)
{ {
var textRunStyle = new GenericTextRunProperties(typeface, fontSize, textDecorations, foreground); var textRunStyle = new GenericTextRunProperties(typeface, fontSize, textDecorations, foreground);
return new GenericTextParagraphProperties(FlowDirection.LeftToRight, textAlignment, true, false, return new GenericTextParagraphProperties(flowDirection, textAlignment, true, false,
textRunStyle, textWrapping, lineHeight, 0); textRunStyle, textWrapping, lineHeight, 0);
} }
@ -306,8 +495,8 @@ namespace Avalonia.Media.TextFormatting
/// <param name="height">The current height.</param> /// <param name="height">The current height.</param>
private static void UpdateBounds(TextLine textLine, ref double width, ref double height) private static void UpdateBounds(TextLine textLine, ref double width, ref double height)
{ {
var lineWidth = textLine.Width + textLine.Start * 2; var lineWidth = textLine.WidthIncludingTrailingWhitespace + textLine.Start * 2;
if (width < lineWidth) if (width < lineWidth)
{ {
width = lineWidth; width = lineWidth;
@ -322,97 +511,97 @@ namespace Avalonia.Media.TextFormatting
/// <returns>The empty text line.</returns> /// <returns>The empty text line.</returns>
private TextLine CreateEmptyTextLine(int startingIndex) private TextLine CreateEmptyTextLine(int startingIndex)
{ {
var flowDirection = _paragraphProperties.FlowDirection;
var properties = _paragraphProperties.DefaultTextRunProperties; var properties = _paragraphProperties.DefaultTextRunProperties;
var glyphTypeface = properties.Typeface.GlyphTypeface;
var text = new ReadOnlySlice<char>(s_empty, startingIndex, 1);
var glyph = glyphTypeface.GetGlyph(s_empty[0]);
var glyphInfos = new[] { new GlyphInfo(glyph, startingIndex) };
var glyphRun = TextShaper.Current.ShapeText(new ReadOnlySlice<char>(s_empty, startingIndex, 1), var shapedBuffer = new ShapedBuffer(text, glyphInfos, glyphTypeface, properties.FontRenderingEmSize,
properties.Typeface, properties.FontRenderingEmSize, properties.CultureInfo); (sbyte)flowDirection);
var textRuns = new List<ShapedTextCharacters> var textRuns = new List<ShapedTextCharacters> { new ShapedTextCharacters(shapedBuffer, properties) };
{
new ShapedTextCharacters(glyphRun, _paragraphProperties.DefaultTextRunProperties)
};
var textRange = new TextRange(startingIndex, 1); var textRange = new TextRange(startingIndex, 1);
return new TextLineImpl(textRuns, textRange, MaxWidth, _paragraphProperties); return new TextLineImpl(textRuns, textRange, MaxWidth, _paragraphProperties, flowDirection).FinalizeLine();
} }
/// <summary> private IReadOnlyList<TextLine> CreateTextLines()
/// Updates the layout and applies specified text style overrides.
/// </summary>
[MemberNotNull(nameof(TextLines))]
private void UpdateLayout()
{ {
if (_text.IsEmpty || MathUtilities.IsZero(MaxWidth) || MathUtilities.IsZero(MaxHeight)) if (_text.IsEmpty || MathUtilities.IsZero(MaxWidth) || MathUtilities.IsZero(MaxHeight))
{ {
var textLine = CreateEmptyTextLine(0); var textLine = CreateEmptyTextLine(0);
TextLines = new List<TextLine> { textLine };
Size = new Size(0, textLine.Height); Size = new Size(0, textLine.Height);
return new List<TextLine> { textLine };
} }
else
{
var textLines = new List<TextLine>();
double width = 0.0, height = 0.0; var textLines = new List<TextLine>();
var currentPosition = 0; double width = 0.0, height = 0.0;
var textSource = new FormattedTextSource(_text, var currentPosition = 0;
_paragraphProperties.DefaultTextRunProperties, _textStyleOverrides);
TextLine? previousLine = null; var textSource = new FormattedTextSource(_text,
_paragraphProperties.DefaultTextRunProperties, _textStyleOverrides);
while (currentPosition < _text.Length) TextLine? previousLine = null;
{
var textLine = TextFormatter.Current.FormatLine(textSource, currentPosition, MaxWidth,
_paragraphProperties, previousLine?.TextLineBreak);
currentPosition += textLine.TextRange.Length; while (currentPosition < _text.Length)
{
var textLine = TextFormatter.Current.FormatLine(textSource, currentPosition, MaxWidth,
_paragraphProperties, previousLine?.TextLineBreak);
if (textLines.Count > 0) currentPosition += textLine.TextRange.Length;
if (textLines.Count > 0)
{
if (textLines.Count == MaxLines || !double.IsPositiveInfinity(MaxHeight) &&
height + textLine.Height > MaxHeight)
{ {
if (textLines.Count == MaxLines || !double.IsPositiveInfinity(MaxHeight) && if (previousLine?.TextLineBreak != null && _textTrimming != TextTrimming.None)
height + textLine.Height > MaxHeight)
{ {
if (previousLine?.TextLineBreak != null && _textTrimming != TextTrimming.None) var collapsedLine =
{ previousLine.Collapse(GetCollapsingProperties(MaxWidth));
var collapsedLine =
previousLine.Collapse(GetCollapsingProperties(MaxWidth));
textLines[textLines.Count - 1] = collapsedLine;
}
break; textLines[textLines.Count - 1] = collapsedLine;
} }
}
var hasOverflowed = textLine.HasOverflowed; break;
if (hasOverflowed && _textTrimming != TextTrimming.None)
{
textLine = textLine.Collapse(GetCollapsingProperties(MaxWidth));
} }
}
textLines.Add(textLine); var hasOverflowed = textLine.HasOverflowed;
UpdateBounds(textLine, ref width, ref height); if (hasOverflowed && _textTrimming != TextTrimming.None)
{
textLine = textLine.Collapse(GetCollapsingProperties(MaxWidth));
}
previousLine = textLine; textLines.Add(textLine);
if (currentPosition == _text.Length && textLine.NewLineLength > 0) UpdateBounds(textLine, ref width, ref height);
{
var emptyTextLine = CreateEmptyTextLine(currentPosition);
textLines.Add(emptyTextLine); previousLine = textLine;
}
if (currentPosition != _text.Length || textLine.NewLineLength <= 0)
{
continue;
} }
Size = new Size(width, height); var emptyTextLine = CreateEmptyTextLine(currentPosition);
TextLines = textLines; textLines.Add(emptyTextLine);
UpdateBounds(emptyTextLine, ref width, ref height);
} }
Size = new Size(width, height);
return textLines;
} }
/// <summary> /// <summary>
@ -431,241 +620,5 @@ namespace Avalonia.Media.TextFormatting
_ => throw new ArgumentOutOfRangeException(), _ => throw new ArgumentOutOfRangeException(),
}; };
} }
public int GetLineIndexFromCharacterIndex(int charIndex)
{
if (TextLines is null)
{
return -1;
}
if (charIndex < 0)
{
return -1;
}
if (charIndex > _text.Length - 1)
{
return TextLines.Count - 1;
}
for (var index = 0; index < TextLines.Count; index++)
{
var textLine = TextLines[index];
if (textLine.TextRange.End < charIndex)
{
continue;
}
if (charIndex >= textLine.Start && charIndex <= textLine.TextRange.End)
{
return index;
}
}
return TextLines.Count - 1;
}
public int GetCharacterIndexFromPoint(Point point, bool snapToText)
{
if (TextLines is null)
{
return -1;
}
var (x, y) = point;
if (!snapToText && y > Size.Height)
{
return -1;
}
var currentY = 0d;
foreach (var textLine in TextLines)
{
if (currentY + textLine.Height <= y)
{
currentY += textLine.Height;
continue;
}
if (x > textLine.WidthIncludingTrailingWhitespace)
{
if (snapToText)
{
return textLine.TextRange.End;
}
return -1;
}
var characterHit = textLine.GetCharacterHitFromDistance(x);
return characterHit.FirstCharacterIndex + characterHit.TrailingLength;
}
return _text.Length;
}
public Rect GetRectFromCharacterIndex(int characterIndex, bool trailingEdge)
{
if (TextLines is null)
{
return Rect.Empty;
}
var distanceY = 0d;
var currentIndex = 0;
foreach (var textLine in TextLines)
{
if (currentIndex + textLine.TextRange.Length < characterIndex)
{
distanceY += textLine.Height;
currentIndex += textLine.TextRange.Length;
continue;
}
var characterHit = new CharacterHit(characterIndex);
while (characterHit.FirstCharacterIndex < characterIndex)
{
characterHit = textLine.GetNextCaretCharacterHit(characterHit);
}
var distanceX = textLine.GetDistanceFromCharacterHit(trailingEdge ?
characterHit :
new CharacterHit(characterHit.FirstCharacterIndex));
if (characterHit.TrailingLength > 0)
{
distanceX += 1;
}
return new Rect(distanceX, distanceY, 0, textLine.Height);
}
return Rect.Empty;
}
private readonly struct FormattedTextSource : ITextSource
{
private readonly ReadOnlySlice<char> _text;
private readonly TextRunProperties _defaultProperties;
private readonly IReadOnlyList<ValueSpan<TextRunProperties>>? _textModifier;
public FormattedTextSource(ReadOnlySlice<char> text, TextRunProperties defaultProperties,
IReadOnlyList<ValueSpan<TextRunProperties>>? textModifier)
{
_text = text;
_defaultProperties = defaultProperties;
_textModifier = textModifier;
}
public TextRun? GetTextRun(int textSourceIndex)
{
if (textSourceIndex > _text.Length)
{
return null;
}
var runText = _text.Skip(textSourceIndex);
if (runText.IsEmpty)
{
return new TextEndOfParagraph();
}
var textStyleRun = CreateTextStyleRun(runText, _defaultProperties, _textModifier);
return new TextCharacters(runText.Take(textStyleRun.Length), textStyleRun.Value);
}
/// <summary>
/// Creates a span of text run properties that has modifier applied.
/// </summary>
/// <param name="text">The text to create the properties for.</param>
/// <param name="defaultProperties">The default text properties.</param>
/// <param name="textModifier">The text properties modifier.</param>
/// <returns>
/// The created text style run.
/// </returns>
private static ValueSpan<TextRunProperties> CreateTextStyleRun(ReadOnlySlice<char> text,
TextRunProperties defaultProperties, IReadOnlyList<ValueSpan<TextRunProperties>>? textModifier)
{
if (textModifier == null || textModifier.Count == 0)
{
return new ValueSpan<TextRunProperties>(text.Start, text.Length, defaultProperties);
}
var currentProperties = defaultProperties;
var hasOverride = false;
var i = 0;
var length = 0;
for (; i < textModifier.Count; i++)
{
var propertiesOverride = textModifier[i];
var textRange = new TextRange(propertiesOverride.Start, propertiesOverride.Length);
if (textRange.End < text.Start)
{
continue;
}
if (textRange.Start > text.End)
{
length = text.Length;
break;
}
if (textRange.Start > text.Start)
{
if (propertiesOverride.Value != currentProperties)
{
length = Math.Min(Math.Abs(textRange.Start - text.Start), text.Length);
break;
}
}
length += Math.Min(text.Length - length, textRange.Length);
if (hasOverride)
{
continue;
}
hasOverride = true;
currentProperties = propertiesOverride.Value;
}
if (length < text.Length && i == textModifier.Count)
{
if (currentProperties == defaultProperties)
{
length = text.Length;
}
}
if (length != text.Length)
{
text = text.Take(length);
}
return new ValueSpan<TextRunProperties>(text.Start, length, currentProperties);
}
}
} }
} }

28
src/Avalonia.Visuals/Media/TextFormatting/TextLine.cs

@ -191,27 +191,45 @@ namespace Avalonia.Media.TextFormatting
/// <summary> /// <summary>
/// Gets the text line offset x. /// Gets the text line offset x.
/// </summary> /// </summary>
/// <param name="lineWidth">The line width.</param> /// <param name="width">The line width.</param>
/// <param name="widthIncludingTrailingWhitespace">The paragraph width including whitespace.</param>
/// <param name="paragraphWidth">The paragraph width.</param> /// <param name="paragraphWidth">The paragraph width.</param>
/// <param name="textAlignment">The text alignment.</param> /// <param name="textAlignment">The text alignment.</param>
/// <param name="flowDirection">The flow direction of the line.</param>
/// <returns>The paragraph offset.</returns> /// <returns>The paragraph offset.</returns>
internal static double GetParagraphOffsetX(double lineWidth, double paragraphWidth, TextAlignment textAlignment) internal static double GetParagraphOffsetX(double width, double widthIncludingTrailingWhitespace,
double paragraphWidth, TextAlignment textAlignment, FlowDirection flowDirection)
{ {
if (double.IsPositiveInfinity(paragraphWidth)) if (double.IsPositiveInfinity(paragraphWidth))
{ {
return 0; return 0;
} }
if (flowDirection == FlowDirection.LeftToRight)
{
switch (textAlignment)
{
case TextAlignment.Center:
return (paragraphWidth - width) / 2;
case TextAlignment.Right:
return paragraphWidth - widthIncludingTrailingWhitespace;
default:
return 0;
}
}
switch (textAlignment) switch (textAlignment)
{ {
case TextAlignment.Center: case TextAlignment.Center:
return (paragraphWidth - lineWidth) / 2; return (paragraphWidth - width) / 2;
case TextAlignment.Right: case TextAlignment.Right:
return paragraphWidth - lineWidth; return 0;
default: default:
return 0.0f; return paragraphWidth - widthIncludingTrailingWhitespace;
} }
} }
} }

17
src/Avalonia.Visuals/Media/TextFormatting/TextLineBreak.cs

@ -4,21 +4,24 @@ namespace Avalonia.Media.TextFormatting
{ {
public class TextLineBreak public class TextLineBreak
{ {
public TextLineBreak(TextEndOfLine textEndOfLine) public TextLineBreak(TextEndOfLine? textEndOfLine = null, FlowDirection flowDirection = FlowDirection.LeftToRight,
IReadOnlyList<ShapedTextCharacters>? remainingCharacters = null)
{ {
TextEndOfLine = textEndOfLine; TextEndOfLine = textEndOfLine;
} FlowDirection = flowDirection;
public TextLineBreak(IReadOnlyList<ShapedTextCharacters> remainingCharacters)
{
RemainingCharacters = remainingCharacters; RemainingCharacters = remainingCharacters;
} }
/// <summary> /// <summary>
/// Get the /// Get the end of line run.
/// </summary> /// </summary>
public TextEndOfLine? TextEndOfLine { get; } public TextEndOfLine? TextEndOfLine { get; }
/// <summary>
/// Get the flow direction for remaining characters.
/// </summary>
public FlowDirection FlowDirection { get; }
/// <summary> /// <summary>
/// Get the remaining shaped characters that were split up by the <see cref="TextFormatter"/> during the formatting process. /// Get the remaining shaped characters that were split up by the <see cref="TextFormatter"/> during the formatting process.
/// </summary> /// </summary>

628
src/Avalonia.Visuals/Media/TextFormatting/TextLineImpl.cs

@ -1,34 +1,40 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using Avalonia.Media.TextFormatting.Unicode; using Avalonia.Media.TextFormatting.Unicode;
using Avalonia.Platform;
using Avalonia.Utilities; using Avalonia.Utilities;
namespace Avalonia.Media.TextFormatting namespace Avalonia.Media.TextFormatting
{ {
internal class TextLineImpl : TextLine internal class TextLineImpl : TextLine
{ {
private readonly List<ShapedTextCharacters> _shapedTextRuns; private static readonly Comparer<int> s_compareStart = Comparer<int>.Default;
private static readonly Comparison<ShapedTextCharacters> s_compareLogicalOrder =
(a, b) => s_compareStart.Compare(a.Text.Start, b.Text.Start);
private readonly List<ShapedTextCharacters> _textRuns;
private readonly double _paragraphWidth; private readonly double _paragraphWidth;
private readonly TextParagraphProperties _paragraphProperties; private readonly TextParagraphProperties _paragraphProperties;
private readonly TextLineMetrics _textLineMetrics; private TextLineMetrics _textLineMetrics;
private readonly FlowDirection _flowDirection;
public TextLineImpl(List<ShapedTextCharacters> textRuns, TextRange textRange, double paragraphWidth, public TextLineImpl(List<ShapedTextCharacters> textRuns, TextRange textRange, double paragraphWidth,
TextParagraphProperties paragraphProperties, TextLineBreak? lineBreak = null, bool hasCollapsed = false) TextParagraphProperties paragraphProperties, FlowDirection flowDirection = FlowDirection.LeftToRight,
TextLineBreak? lineBreak = null, bool hasCollapsed = false)
{ {
TextRange = textRange; TextRange = textRange;
TextLineBreak = lineBreak; TextLineBreak = lineBreak;
HasCollapsed = hasCollapsed; HasCollapsed = hasCollapsed;
_shapedTextRuns = textRuns; _textRuns = textRuns;
_paragraphWidth = paragraphWidth; _paragraphWidth = paragraphWidth;
_paragraphProperties = paragraphProperties; _paragraphProperties = paragraphProperties;
_textLineMetrics = CreateLineMetrics(); _flowDirection = flowDirection;
} }
/// <inheritdoc/> /// <inheritdoc/>
public override IReadOnlyList<TextRun> TextRuns => _shapedTextRuns; public override IReadOnlyList<TextRun> TextRuns => _textRuns;
/// <inheritdoc/> /// <inheritdoc/>
public override TextRange TextRange { get; } public override TextRange TextRange { get; }
@ -80,12 +86,12 @@ namespace Avalonia.Media.TextFormatting
{ {
var (currentX, currentY) = lineOrigin; var (currentX, currentY) = lineOrigin;
foreach (var textRun in _shapedTextRuns) foreach (var textRun in _textRuns)
{ {
var offsetY = Baseline - textRun.GlyphRun.BaselineOrigin.Y; var offsetY = Baseline - textRun.GlyphRun.BaselineOrigin.Y;
textRun.Draw(drawingContext, new Point(currentX, currentY + offsetY)); textRun.Draw(drawingContext, new Point(currentX, currentY + offsetY));
currentX += textRun.Size.Width; currentX += textRun.Size.Width;
} }
} }
@ -93,7 +99,7 @@ namespace Avalonia.Media.TextFormatting
/// <inheritdoc/> /// <inheritdoc/>
public override TextLine Collapse(params TextCollapsingProperties[] collapsingPropertiesList) public override TextLine Collapse(params TextCollapsingProperties[] collapsingPropertiesList)
{ {
if (collapsingPropertiesList == null || collapsingPropertiesList.Length == 0) if (collapsingPropertiesList.Length == 0)
{ {
return this; return this;
} }
@ -105,21 +111,22 @@ namespace Avalonia.Media.TextFormatting
var textRange = TextRange; var textRange = TextRange;
var collapsedLength = 0; var collapsedLength = 0;
var shapedSymbol = CreateShapedSymbol(collapsingProperties.Symbol); var shapedSymbol = TextFormatterImpl.CreateSymbol(collapsingProperties.Symbol, _paragraphProperties.FlowDirection);
var availableWidth = collapsingProperties.Width - shapedSymbol.Size.Width; var availableWidth = collapsingProperties.Width - shapedSymbol.GlyphRun.Size.Width;
while (runIndex < _shapedTextRuns.Count) while (runIndex < _textRuns.Count)
{ {
var currentRun = _shapedTextRuns[runIndex]; var currentRun = _textRuns[runIndex];
currentWidth += currentRun.Size.Width; currentWidth += currentRun.Size.Width;
if (currentWidth > availableWidth) if (currentWidth > availableWidth)
{ {
if (TextFormatterImpl.TryMeasureCharacters(currentRun, availableWidth, out var measuredLength)) if (currentRun.TryMeasureCharacters(availableWidth, out var measuredLength))
{ {
if (collapsingProperties.Style == TextCollapsingStyle.TrailingWord && measuredLength < textRange.End) if (collapsingProperties.Style == TextCollapsingStyle.TrailingWord &&
measuredLength < textRange.End)
{ {
var currentBreakPosition = 0; var currentBreakPosition = 0;
@ -148,18 +155,22 @@ namespace Avalonia.Media.TextFormatting
collapsedLength += measuredLength; collapsedLength += measuredLength;
var splitResult = TextFormatterImpl.SplitTextRuns(_shapedTextRuns, collapsedLength); var splitResult = TextFormatterImpl.SplitShapedRuns(_textRuns, collapsedLength);
var shapedTextCharacters = new List<ShapedTextCharacters>(splitResult.First.Count + 1); var shapedTextCharacters = new List<ShapedTextCharacters>(splitResult.First.Count + 1);
shapedTextCharacters.AddRange(splitResult.First); shapedTextCharacters.AddRange(splitResult.First);
SortRuns(shapedTextCharacters);
shapedTextCharacters.Add(shapedSymbol); shapedTextCharacters.Add(shapedSymbol);
textRange = new TextRange(textRange.Start, collapsedLength); textRange = new TextRange(textRange.Start, collapsedLength);
return new TextLineImpl(shapedTextCharacters, textRange, _paragraphWidth, _paragraphProperties, var textLine = new TextLineImpl(shapedTextCharacters, textRange, _paragraphWidth, _paragraphProperties,
TextLineBreak, true); _flowDirection, TextLineBreak, true);
return textLine.FinalizeLine();
} }
availableWidth -= currentRun.Size.Width; availableWidth -= currentRun.Size.Width;
@ -172,78 +183,23 @@ namespace Avalonia.Media.TextFormatting
return this; return this;
} }
private TextLineMetrics CreateLineMetrics()
{
var width = 0d;
var widthIncludingWhitespace = 0d;
var trailingWhitespaceLength = 0;
var newLineLength = 0;
var ascent = 0d;
var descent = 0d;
var lineGap = 0d;
for (var index = 0; index < _shapedTextRuns.Count; index++)
{
var textRun = _shapedTextRuns[index];
var fontMetrics =
new FontMetrics(textRun.Properties.Typeface, textRun.Properties.FontRenderingEmSize);
if (ascent > fontMetrics.Ascent)
{
ascent = fontMetrics.Ascent;
}
if (descent < fontMetrics.Descent)
{
descent = fontMetrics.Descent;
}
if (lineGap < fontMetrics.LineGap)
{
lineGap = fontMetrics.LineGap;
}
if (index == _shapedTextRuns.Count - 1)
{
width = widthIncludingWhitespace + textRun.GlyphRun.Metrics.Width;
widthIncludingWhitespace += textRun.GlyphRun.Metrics.WidthIncludingTrailingWhitespace;
trailingWhitespaceLength = textRun.GlyphRun.Metrics.TrailingWhitespaceLength;
newLineLength = textRun.GlyphRun.Metrics.NewlineLength;
}
else
{
widthIncludingWhitespace += textRun.GlyphRun.Metrics.WidthIncludingTrailingWhitespace;
}
}
var start = GetParagraphOffsetX(width, _paragraphWidth, _paragraphProperties.TextAlignment);
var lineHeight = _paragraphProperties.LineHeight;
var height = double.IsNaN(lineHeight) || MathUtilities.IsZero(lineHeight) ?
descent - ascent + lineGap :
lineHeight;
return new TextLineMetrics(widthIncludingWhitespace > _paragraphWidth, height, newLineLength, start,
-ascent, trailingWhitespaceLength, width, widthIncludingWhitespace);
}
/// <inheritdoc/> /// <inheritdoc/>
public override CharacterHit GetCharacterHitFromDistance(double distance) public override CharacterHit GetCharacterHitFromDistance(double distance)
{ {
distance -= Start; distance -= Start;
if (distance < 0) if (distance <= 0)
{ {
// hit happens before the line, return the first position // hit happens before the line, return the first position
return new CharacterHit(TextRange.Start); var firstRun = _textRuns[0];
return firstRun.GlyphRun.GetCharacterHitFromDistance(distance, out _);
} }
// process hit that happens within the line // process hit that happens within the line
var characterHit = new CharacterHit(); var characterHit = new CharacterHit();
foreach (var run in _shapedTextRuns) foreach (var run in _textRuns)
{ {
characterHit = run.GlyphRun.GetCharacterHitFromDistance(distance, out _); characterHit = run.GlyphRun.GetCharacterHitFromDistance(distance, out _);
@ -263,27 +219,90 @@ namespace Avalonia.Media.TextFormatting
{ {
var characterIndex = characterHit.FirstCharacterIndex + (characterHit.TrailingLength != 0 ? 1 : 0); var characterIndex = characterHit.FirstCharacterIndex + (characterHit.TrailingLength != 0 ? 1 : 0);
if (characterIndex > TextRange.End) var currentDistance = Start;
GlyphRun? lastRun = null;
for (var index = 0; index < _textRuns.Count; index++)
{ {
if (NewLineLength > 0) var textRun = _textRuns[index];
var currentRun = textRun.GlyphRun;
if (lastRun != null)
{ {
return Start + Width; if (!lastRun.IsLeftToRight && currentRun.IsLeftToRight &&
currentRun.Characters.Start == characterHit.FirstCharacterIndex &&
characterHit.TrailingLength == 0)
{
return currentDistance;
}
} }
return Start + WidthIncludingTrailingWhitespace;
}
var currentDistance = Start; //Look for a hit in within the current run
if (characterIndex >= textRun.Text.Start && characterIndex <= textRun.Text.End)
{
var distance = currentRun.GetDistanceFromCharacterHit(characterHit);
foreach (var textRun in _shapedTextRuns) return currentDistance + distance;
{ }
if (characterIndex > textRun.Text.End)
//Look at the left and right edge of the current run
if (currentRun.IsLeftToRight)
{ {
currentDistance += textRun.Size.Width; if (lastRun == null || lastRun.IsLeftToRight)
{
if (characterIndex <= textRun.Text.Start)
{
return currentDistance;
}
}
else
{
if (characterIndex == textRun.Text.Start)
{
return currentDistance;
}
}
continue; if (characterIndex == textRun.Text.Start + textRun.Text.Length && characterHit.TrailingLength > 0)
{
return currentDistance + currentRun.Size.Width;
}
} }
else
{
if (characterIndex == textRun.Text.Start)
{
return currentDistance + currentRun.Size.Width;
}
var nextRun = index + 1 < _textRuns.Count ? _textRuns[index + 1] : null;
if (nextRun != null)
{
if (characterHit.FirstCharacterIndex == textRun.Text.End && nextRun.ShapedBuffer.IsLeftToRight)
{
return currentDistance;
}
return currentDistance + textRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(characterIndex)); if (characterIndex > textRun.Text.End && nextRun.Text.End < textRun.Text.End)
{
return currentDistance;
}
}
else
{
if (characterIndex > textRun.Text.End)
{
return currentDistance;
}
}
}
//No hit hit found so we add the full width
currentDistance += currentRun.Size.Width;
lastRun = currentRun;
} }
return currentDistance; return currentDistance;
@ -297,18 +316,14 @@ namespace Avalonia.Media.TextFormatting
return nextCharacterHit; return nextCharacterHit;
} }
if (characterHit.FirstCharacterIndex + characterHit.TrailingLength <= TextRange.Start + TextRange.Length) // Can't move, we're after the last character
{ var runIndex = GetRunIndexAtCharacterIndex(TextRange.End, LogicalDirection.Forward);
return characterHit; // Can't move, we're after the last character
}
var runIndex = GetRunIndexAtCodepointIndex(TextRange.End); var textRun = _textRuns[runIndex];
var textRun = _shapedTextRuns[runIndex];
characterHit = textRun.GlyphRun.GetNextCaretCharacterHit(characterHit); characterHit = textRun.GlyphRun.GetNextCaretCharacterHit(characterHit);
return characterHit; // Can't move, we're after the last character return characterHit;
} }
/// <inheritdoc/> /// <inheritdoc/>
@ -319,7 +334,7 @@ namespace Avalonia.Media.TextFormatting
return previousCharacterHit; return previousCharacterHit;
} }
if (characterHit.FirstCharacterIndex < TextRange.Start) if (characterHit.FirstCharacterIndex <= TextRange.Start)
{ {
characterHit = new CharacterHit(TextRange.Start); characterHit = new CharacterHit(TextRange.Start);
} }
@ -334,6 +349,170 @@ namespace Avalonia.Media.TextFormatting
return GetPreviousCaretCharacterHit(characterHit); return GetPreviousCaretCharacterHit(characterHit);
} }
public static void SortRuns(List<ShapedTextCharacters> textRuns)
{
textRuns.Sort(s_compareLogicalOrder);
}
public TextLineImpl FinalizeLine()
{
BidiReorder();
_textLineMetrics = CreateLineMetrics();
return this;
}
private void BidiReorder()
{
// Build up the collection of ordered runs.
var run = _textRuns[0];
OrderedBidiRun orderedRun = new(run);
var current = orderedRun;
for (var i = 1; i < _textRuns.Count; i++)
{
run = _textRuns[i];
current.Next = new OrderedBidiRun(run);
current = current.Next;
}
// Reorder them into visual order.
orderedRun = LinearReOrder(orderedRun);
// Now perform a recursive reversal of each run.
// From the highest level found in the text to the lowest odd level on each line, including intermediate levels
// not actually present in the text, reverse any contiguous sequence of characters that are at that level or higher.
// https://unicode.org/reports/tr9/#L2
sbyte max = 0;
var min = sbyte.MaxValue;
for (var i = 0; i < _textRuns.Count; i++)
{
var level = _textRuns[i].BidiLevel;
if (level > max)
{
max = level;
}
if ((level & 1) != 0 && level < min)
{
min = level;
}
}
if (min > max)
{
min = max;
}
if (max == 0 || (min == max && (max & 1) == 0))
{
// Nothing to reverse.
return;
}
// Now apply the reversal and replace the original contents.
var minLevelToReverse = max;
while (minLevelToReverse >= min)
{
current = orderedRun;
while (current != null)
{
if (current.Level >= minLevelToReverse && current.Level % 2 != 0)
{
if (!current.Run.IsReversed)
{
current.Run.Reverse();
}
}
current = current.Next;
}
minLevelToReverse--;
}
_textRuns.Clear();
current = orderedRun;
while (current != null)
{
_textRuns.Add(current.Run);
current = current.Next;
}
}
/// <summary>
/// Reorders a series of runs from logical to visual order, returning the left most run.
/// <see href="https://github.com/fribidi/linear-reorder/blob/f2f872257d4d8b8e137fcf831f254d6d4db79d3c/linear-reorder.c"/>
/// </summary>
/// <param name="run">The ordered bidi run.</param>
/// <returns>The <see cref="OrderedBidiRun"/>.</returns>
private static OrderedBidiRun LinearReOrder(OrderedBidiRun? run)
{
BidiRange? range = null;
while (run != null)
{
var next = run.Next;
while (range != null && range.Level > run.Level
&& range.Previous != null && range.Previous.Level >= run.Level)
{
range = BidiRange.MergeWithPrevious(range);
}
if (range != null && range.Level >= run.Level)
{
// Attach run to the range.
if ((run.Level & 1) != 0)
{
// Odd, range goes to the right of run.
run.Next = range.Left;
range.Left = run;
}
else
{
// Even, range goes to the left of run.
range.Right!.Next = run;
range.Right = run;
}
range.Level = run.Level;
}
else
{
var r = new BidiRange();
r.Left = r.Right = run;
r.Level = run.Level;
r.Previous = range;
range = r;
}
run = next;
}
while (range?.Previous != null)
{
range = BidiRange.MergeWithPrevious(range);
}
// Terminate.
range!.Right!.Next = null;
return range.Left!;
}
/// <summary> /// <summary>
/// Tries to find the next character hit. /// Tries to find the next character hit.
/// </summary> /// </summary>
@ -346,7 +525,7 @@ namespace Avalonia.Media.TextFormatting
var codepointIndex = characterHit.FirstCharacterIndex + characterHit.TrailingLength; var codepointIndex = characterHit.FirstCharacterIndex + characterHit.TrailingLength;
if (codepointIndex > TextRange.End) if (codepointIndex >= TextRange.End)
{ {
return false; // Cannot go forward anymore return false; // Cannot go forward anymore
} }
@ -356,21 +535,29 @@ namespace Avalonia.Media.TextFormatting
codepointIndex = TextRange.Start; codepointIndex = TextRange.Start;
} }
var runIndex = GetRunIndexAtCodepointIndex(codepointIndex); var runIndex = GetRunIndexAtCharacterIndex(codepointIndex, LogicalDirection.Forward);
while (runIndex < _shapedTextRuns.Count) while (runIndex < _textRuns.Count)
{ {
var run = _shapedTextRuns[runIndex]; var run = _textRuns[runIndex];
var foundCharacterHit = var foundCharacterHit =
run.GlyphRun.FindNearestCharacterHit(characterHit.FirstCharacterIndex + characterHit.TrailingLength, out _); run.GlyphRun.FindNearestCharacterHit(characterHit.FirstCharacterIndex + characterHit.TrailingLength,
out _);
var isAtEnd = foundCharacterHit.FirstCharacterIndex + foundCharacterHit.TrailingLength == var isAtEnd = foundCharacterHit.FirstCharacterIndex + foundCharacterHit.TrailingLength ==
TextRange.Length; TextRange.Length;
if (isAtEnd && !run.GlyphRun.IsLeftToRight)
{
nextCharacterHit = foundCharacterHit;
return true;
}
var characterIndex = codepointIndex - run.Text.Start; var characterIndex = codepointIndex - run.Text.Start;
if (characterIndex < 0 && characterHit.TrailingLength == 0) if (characterIndex < 0 && run.ShapedBuffer.IsLeftToRight)
{ {
foundCharacterHit = new CharacterHit(foundCharacterHit.FirstCharacterIndex); foundCharacterHit = new CharacterHit(foundCharacterHit.FirstCharacterIndex);
} }
@ -398,7 +585,9 @@ namespace Avalonia.Media.TextFormatting
/// <returns></returns> /// <returns></returns>
private bool TryFindPreviousCharacterHit(CharacterHit characterHit, out CharacterHit previousCharacterHit) private bool TryFindPreviousCharacterHit(CharacterHit characterHit, out CharacterHit previousCharacterHit)
{ {
if (characterHit.FirstCharacterIndex == TextRange.Start) var characterIndex = characterHit.FirstCharacterIndex + characterHit.TrailingLength;
if (characterIndex == TextRange.Start)
{ {
previousCharacterHit = new CharacterHit(TextRange.Start); previousCharacterHit = new CharacterHit(TextRange.Start);
@ -407,26 +596,32 @@ namespace Avalonia.Media.TextFormatting
previousCharacterHit = characterHit; previousCharacterHit = characterHit;
var codepointIndex = characterHit.FirstCharacterIndex + characterHit.TrailingLength; if (characterIndex < TextRange.Start)
if (codepointIndex < TextRange.Start)
{ {
return false; // Cannot go backward anymore. return false; // Cannot go backward anymore.
} }
var runIndex = GetRunIndexAtCodepointIndex(codepointIndex); var runIndex = GetRunIndexAtCharacterIndex(characterIndex, LogicalDirection.Backward);
while (runIndex >= 0) while (runIndex >= 0)
{ {
var run = _shapedTextRuns[runIndex]; var run = _textRuns[runIndex];
var foundCharacterHit = run.GlyphRun.FindNearestCharacterHit(characterHit.FirstCharacterIndex - 1, out _); var foundCharacterHit =
run.GlyphRun.FindNearestCharacterHit(characterHit.FirstCharacterIndex - 1, out _);
if (foundCharacterHit.FirstCharacterIndex + foundCharacterHit.TrailingLength < characterIndex)
{
previousCharacterHit = foundCharacterHit;
return true;
}
previousCharacterHit = characterHit.TrailingLength != 0 ? previousCharacterHit = characterHit.TrailingLength != 0 ?
foundCharacterHit : foundCharacterHit :
new CharacterHit(foundCharacterHit.FirstCharacterIndex); new CharacterHit(foundCharacterHit.FirstCharacterIndex);
if (previousCharacterHit.FirstCharacterIndex < characterHit.FirstCharacterIndex) if (previousCharacterHit != characterHit)
{ {
return true; return true;
} }
@ -441,52 +636,203 @@ namespace Avalonia.Media.TextFormatting
/// Gets the run index of the specified codepoint index. /// Gets the run index of the specified codepoint index.
/// </summary> /// </summary>
/// <param name="codepointIndex">The codepoint index.</param> /// <param name="codepointIndex">The codepoint index.</param>
/// <param name="direction">The logical direction.</param>
/// <returns>The text run index.</returns> /// <returns>The text run index.</returns>
private int GetRunIndexAtCodepointIndex(int codepointIndex) private int GetRunIndexAtCharacterIndex(int codepointIndex, LogicalDirection direction)
{ {
if (codepointIndex > TextRange.End) var runIndex = 0;
{ ShapedTextCharacters? previousRun = null;
return _shapedTextRuns.Count - 1;
}
if (codepointIndex <= 0) while (runIndex < _textRuns.Count)
{ {
return 0; var currentRun = _textRuns[runIndex];
if (previousRun != null && !previousRun.ShapedBuffer.IsLeftToRight)
{
if (currentRun.ShapedBuffer.IsLeftToRight)
{
if (currentRun.Text.Start >= codepointIndex)
{
return --runIndex;
}
}
else
{
if (codepointIndex > currentRun.Text.Start + currentRun.Text.Length)
{
return --runIndex;
}
}
}
if (direction == LogicalDirection.Forward)
{
if (codepointIndex >= currentRun.Text.Start && codepointIndex <= currentRun.Text.End)
{
return runIndex;
}
}
else
{
if (codepointIndex > currentRun.Text.Start &&
codepointIndex <= currentRun.Text.Start + currentRun.Text.Length)
{
return runIndex;
}
}
if (runIndex + 1 < _textRuns.Count)
{
runIndex++;
previousRun = currentRun;
}
else
{
break;
}
} }
var runIndex = 0; return runIndex;
}
while (runIndex < _shapedTextRuns.Count) private TextLineMetrics CreateLineMetrics()
{
var width = 0d;
var widthIncludingWhitespace = 0d;
var trailingWhitespaceLength = 0;
var newLineLength = 0;
var ascent = 0d;
var descent = 0d;
var lineGap = 0d;
var fontRenderingEmSize = 0d;
for (var index = 0; index < _textRuns.Count; index++)
{ {
var run = _shapedTextRuns[runIndex]; var textRun = _textRuns[index];
if (run.Text.End >= codepointIndex) var fontMetrics =
new FontMetrics(textRun.Properties.Typeface, textRun.Properties.FontRenderingEmSize);
if (fontRenderingEmSize < textRun.Properties.FontRenderingEmSize)
{ {
return runIndex; fontRenderingEmSize = textRun.Properties.FontRenderingEmSize;
if (ascent > fontMetrics.Ascent)
{
ascent = fontMetrics.Ascent;
}
if (descent < fontMetrics.Descent)
{
descent = fontMetrics.Descent;
}
if (lineGap < fontMetrics.LineGap)
{
lineGap = fontMetrics.LineGap;
}
} }
runIndex++; switch (_paragraphProperties.FlowDirection)
{
case FlowDirection.LeftToRight:
{
if (index == _textRuns.Count - 1)
{
width = widthIncludingWhitespace + textRun.GlyphRun.Metrics.Width;
trailingWhitespaceLength = textRun.GlyphRun.Metrics.TrailingWhitespaceLength;
newLineLength = textRun.GlyphRun.Metrics.NewlineLength;
}
break;
}
case FlowDirection.RightToLeft:
{
if (index == _textRuns.Count - 1)
{
var firstRun = _textRuns[0];
var offset = firstRun.GlyphRun.Metrics.WidthIncludingTrailingWhitespace -
firstRun.GlyphRun.Metrics.Width;
width = widthIncludingWhitespace +
textRun.GlyphRun.Metrics.WidthIncludingTrailingWhitespace - offset;
trailingWhitespaceLength = firstRun.GlyphRun.Metrics.TrailingWhitespaceLength;
newLineLength = firstRun.GlyphRun.Metrics.NewlineLength;
}
break;
}
}
widthIncludingWhitespace += textRun.GlyphRun.Metrics.WidthIncludingTrailingWhitespace;
} }
return runIndex; var start = GetParagraphOffsetX(width, widthIncludingWhitespace, _paragraphWidth,
_paragraphProperties.TextAlignment, _paragraphProperties.FlowDirection);
var lineHeight = _paragraphProperties.LineHeight;
var height = double.IsNaN(lineHeight) || MathUtilities.IsZero(lineHeight) ?
descent - ascent + lineGap :
lineHeight;
return new TextLineMetrics(widthIncludingWhitespace > _paragraphWidth, height, newLineLength, start,
-ascent, trailingWhitespaceLength, width, widthIncludingWhitespace);
} }
/// <summary> private sealed class OrderedBidiRun
/// Creates a shaped symbol.
/// </summary>
/// <param name="textRun">The symbol run to shape.</param>
/// <returns>
/// The shaped symbol.
/// </returns>
internal static ShapedTextCharacters CreateShapedSymbol(TextRun textRun)
{ {
var properties = textRun.Properties; public OrderedBidiRun(ShapedTextCharacters run) => Run = run;
public sbyte Level => Run.BidiLevel;
_ = properties ?? throw new InvalidOperationException($"{nameof(TextRun.Properties)} should not be null."); public ShapedTextCharacters Run { get; }
var glyphRun = TextShaper.Current.ShapeText(textRun.Text, properties.Typeface, properties.FontRenderingEmSize, properties.CultureInfo); public OrderedBidiRun? Next { get; set; }
return new ShapedTextCharacters(glyphRun, properties); public void Reverse() => Run.ShapedBuffer.GlyphInfos.Span.Reverse();
}
private sealed class BidiRange
{
public int Level { get; set; }
public OrderedBidiRun? Left { get; set; }
public OrderedBidiRun? Right { get; set; }
public BidiRange? Previous { get; set; }
public static BidiRange MergeWithPrevious(BidiRange range)
{
var previous = range.Previous;
BidiRange left;
BidiRange right;
if ((previous!.Level & 1) != 0)
{
// Odd, previous goes to the right of range.
left = range;
right = previous;
}
else
{
// Even, previous goes to the left of range.
left = previous;
right = range;
}
// Stitch them
left.Right!.Next = right.Left;
previous.Left = left.Left;
previous.Right = right.Right;
return previous;
}
} }
} }
} }

2
src/Avalonia.Visuals/Media/TextFormatting/TextParagraphProperties.cs

@ -7,7 +7,7 @@
{ {
/// <summary> /// <summary>
/// This property specifies whether the primary text advance /// This property specifies whether the primary text advance
/// direction shall be left-to-right, right-to-left, or top-to-bottom. /// direction shall be left-to-right, right-to-left.
/// </summary> /// </summary>
public abstract FlowDirection FlowDirection { get; } public abstract FlowDirection FlowDirection { get; }

2
src/Avalonia.Visuals/Media/TextFormatting/TextRun.cs

@ -41,7 +41,7 @@ namespace Avalonia.Media.TextFormatting
{ {
unsafe unsafe
{ {
fixed (char* charsPtr = _textRun.Text.Buffer.Span) fixed (char* charsPtr = _textRun.Text.Span)
{ {
return new string(charsPtr, 0, _textRun.Text.Length); return new string(charsPtr, 0, _textRun.Text.Length);
} }

6
src/Avalonia.Visuals/Media/TextFormatting/TextShaper.cs

@ -45,10 +45,10 @@ namespace Avalonia.Media.TextFormatting
} }
/// <inheritdoc cref="ITextShaperImpl.ShapeText"/> /// <inheritdoc cref="ITextShaperImpl.ShapeText"/>
public GlyphRun ShapeText(ReadOnlySlice<char> text, Typeface typeface, double fontRenderingEmSize, public ShapedBuffer ShapeText(ReadOnlySlice<char> text, GlyphTypeface typeface, double fontRenderingEmSize,
CultureInfo? culture) CultureInfo? culture, sbyte bidiLevel)
{ {
return _platformImpl.ShapeText(text, typeface, fontRenderingEmSize, culture); return _platformImpl.ShapeText(text, typeface, fontRenderingEmSize, culture, bidiLevel);
} }
} }
} }

1717
src/Avalonia.Visuals/Media/TextFormatting/Unicode/BiDiAlgorithm.cs

File diff suppressed because it is too large

2
src/Avalonia.Visuals/Media/TextFormatting/Unicode/BiDiClass.cs

@ -1,6 +1,6 @@
namespace Avalonia.Media.TextFormatting.Unicode namespace Avalonia.Media.TextFormatting.Unicode
{ {
public enum BiDiClass public enum BidiClass
{ {
LeftToRight, //L LeftToRight, //L
ArabicLetter, //AL ArabicLetter, //AL

182
src/Avalonia.Visuals/Media/TextFormatting/Unicode/BiDiData.cs

@ -0,0 +1,182 @@
// Copyright (c) Six Labors.
// Licensed under the Apache License, Version 2.0.
// Ported from: https://github.com/SixLabors/Fonts/
using Avalonia.Utilities;
namespace Avalonia.Media.TextFormatting.Unicode
{
/// <summary>
/// Represents a unicode string and all associated attributes
/// for each character required for the bidirectional Unicode algorithm
/// </summary>
internal class BidiData
{
private ArrayBuilder<BidiClass> _classes;
private ArrayBuilder<BidiPairedBracketType> _pairedBracketTypes;
private ArrayBuilder<int> _pairedBracketValues;
private ArrayBuilder<BidiClass> _savedClasses;
private ArrayBuilder<BidiPairedBracketType> _savedPairedBracketTypes;
private ArrayBuilder<sbyte> _tempLevelBuffer;
public BidiData(sbyte paragraphEmbeddingLevel = 0)
{
ParagraphEmbeddingLevel = paragraphEmbeddingLevel;
}
public BidiData(ReadOnlySlice<char> text, sbyte paragraphEmbeddingLevel = 0) : this(paragraphEmbeddingLevel)
{
Append(text);
}
public sbyte ParagraphEmbeddingLevel { get; private set; }
public bool HasBrackets { get; private set; }
public bool HasEmbeddings { get; private set; }
public bool HasIsolates { get; private set; }
/// <summary>
/// Gets the length of the data held by the BidiData
/// </summary>
public int Length{get; private set; }
/// <summary>
/// Gets the bidi character type of each code point
/// </summary>
public ArraySlice<BidiClass> Classes { get; private set; }
/// <summary>
/// Gets the paired bracket type for each code point
/// </summary>
public ArraySlice<BidiPairedBracketType> PairedBracketTypes { get; private set; }
/// <summary>
/// Gets the paired bracket value for code point
/// </summary>
/// <remarks>
/// The paired bracket values are the code points
/// of each character where the opening code point
/// is replaced with the closing code point for easier
/// matching. Also, bracket code points are mapped
/// to their canonical equivalents
/// </remarks>
public ArraySlice<int> PairedBracketValues { get; private set; }
public void Append(ReadOnlySlice<char> text)
{
_classes.Add(text.Length);
_pairedBracketTypes.Add(text.Length);
_pairedBracketValues.Add(text.Length);
var i = Length;
var codePointEnumerator = new CodepointEnumerator(text);
while (codePointEnumerator.MoveNext())
{
var codepoint = codePointEnumerator.Current;
// Look up BiDiClass
var dir = codepoint.BiDiClass;
_classes[i] = dir;
switch (dir)
{
case BidiClass.LeftToRightEmbedding:
case BidiClass.LeftToRightOverride:
case BidiClass.RightToLeftEmbedding:
case BidiClass.RightToLeftOverride:
case BidiClass.PopDirectionalFormat:
{
HasEmbeddings = true;
break;
}
case BidiClass.LeftToRightIsolate:
case BidiClass.RightToLeftIsolate:
case BidiClass.FirstStrongIsolate:
case BidiClass.PopDirectionalIsolate:
{
HasIsolates = true;
break;
}
}
// Lookup paired bracket types
var pbt = codepoint.PairedBracketType;
_pairedBracketTypes[i] = pbt;
if (pbt == BidiPairedBracketType.Open)
{
// Opening bracket types can never have a null pairing.
codepoint.TryGetPairedBracket(out var paired);
_pairedBracketValues[i] = Codepoint.GetCanonicalType(paired).Value;
HasBrackets = true;
}
else if (pbt == BidiPairedBracketType.Close)
{
_pairedBracketValues[i] = Codepoint.GetCanonicalType(codepoint).Value;
HasBrackets = true;
}
i++;
}
Length = i;
Classes = _classes.AsSlice(0, Length);
PairedBracketTypes = _pairedBracketTypes.AsSlice(0, Length);
PairedBracketValues = _pairedBracketValues.AsSlice(0, Length);
}
/// <summary>
/// Save the Types and PairedBracketTypes of this BiDiData
/// </summary>
/// <remarks>
/// This is used when processing embedded style runs with
/// BiDiClass overrides. Text layout process saves the data,
/// overrides the style runs to neutral, processes the bidi
/// data for the entire paragraph and then restores this data
/// before processing the embedded runs.
/// </remarks>
public void SaveTypes()
{
// Capture the types data
_savedClasses.Clear();
_savedClasses.Add(_classes.AsSlice());
_savedPairedBracketTypes.Clear();
_savedPairedBracketTypes.Add(_pairedBracketTypes.AsSlice());
}
/// <summary>
/// Restore the data saved by SaveTypes
/// </summary>
public void RestoreTypes()
{
_classes.Clear();
_classes.Add(_savedClasses.AsSlice());
_pairedBracketTypes.Clear();
_pairedBracketTypes.Add(_savedPairedBracketTypes.AsSlice());
}
/// <summary>
/// Gets a temporary level buffer. Used by the text layout process when
/// resolving style runs with different BiDiClass.
/// </summary>
/// <param name="length">Length of the required ExpandableBuffer</param>
/// <returns>An uninitialized level ExpandableBuffer</returns>
public ArraySlice<sbyte> GetTempLevelBuffer(int length)
{
_tempLevelBuffer.Clear();
return _tempLevelBuffer.Add(length, false);
}
}
}

9
src/Avalonia.Visuals/Media/TextFormatting/Unicode/BiDiPairedBracketType.cs

@ -0,0 +1,9 @@
namespace Avalonia.Media.TextFormatting.Unicode
{
public enum BidiPairedBracketType
{
None, //n
Close, //c
Open, //o
}
}

58
src/Avalonia.Visuals/Media/TextFormatting/Unicode/Codepoint.cs

@ -1,4 +1,5 @@
using Avalonia.Utilities; using System.Runtime.CompilerServices;
using Avalonia.Utilities;
namespace Avalonia.Media.TextFormatting.Unicode namespace Avalonia.Media.TextFormatting.Unicode
{ {
@ -30,10 +31,15 @@ namespace Avalonia.Media.TextFormatting.Unicode
public Script Script => UnicodeData.GetScript(Value); public Script Script => UnicodeData.GetScript(Value);
/// <summary> /// <summary>
/// Gets the <see cref="Unicode.BiDiClass"/>. /// Gets the <see cref="Unicode.BidiClass"/>.
/// </summary> /// </summary>
public BiDiClass BiDiClass => UnicodeData.GetBiDiClass(Value); public BidiClass BiDiClass => UnicodeData.GetBiDiClass(Value);
/// <summary>
/// Gets the <see cref="Unicode.BidiPairedBracketType"/>.
/// </summary>
public BidiPairedBracketType PairedBracketType => UnicodeData.GetBiDiPairedBracketType(Value);
/// <summary> /// <summary>
/// Gets the <see cref="Unicode.LineBreakClass"/>. /// Gets the <see cref="Unicode.LineBreakClass"/>.
/// </summary> /// </summary>
@ -93,6 +99,52 @@ namespace Avalonia.Media.TextFormatting.Unicode
return false; return false;
} }
} }
/// <summary>
/// Gets the canonical representation of a given codepoint.
/// <see href="http://www.unicode.org/L2/L2013/13123-norm-and-bpa.pdf"/>
/// </summary>
/// <param name="codePoint">The code point to be mapped.</param>
/// <returns>The mapped canonical code point, or the passed <paramref name="codePoint"/>.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal static Codepoint GetCanonicalType(Codepoint codePoint)
{
if (codePoint.Value == 0x3008)
{
return new Codepoint(0x2329);
}
if (codePoint.Value == 0x3009)
{
return new Codepoint(0x232A);
}
return codePoint;
}
/// <summary>
/// Gets the codepoint representing the bracket pairing for this instance.
/// </summary>
/// <param name="codepoint">
/// When this method returns, contains the codepoint representing the bracket pairing for this instance;
/// otherwise, the default value for the type of the <paramref name="codepoint"/> parameter.
/// This parameter is passed uninitialized.
/// .</param>
/// <returns><see langword="true"/> if this instance has a bracket pairing; otherwise, <see langword="false"/></returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool TryGetPairedBracket(out Codepoint codepoint)
{
if (PairedBracketType == BidiPairedBracketType.None)
{
codepoint = default;
return false;
}
codepoint = UnicodeData.GetBiDiPairedBracket(Value);
return true;
}
public static implicit operator int(Codepoint codepoint) public static implicit operator int(Codepoint codepoint)
{ {

331
src/Avalonia.Visuals/Media/TextFormatting/Unicode/PropertyValueAliasHelper.cs

@ -174,5 +174,336 @@ namespace Avalonia.Media.TextFormatting.Unicode
} }
return s_scriptToTag[script]; return s_scriptToTag[script];
} }
private static readonly Dictionary<string, Script> s_tagToScript =
new Dictionary<string,Script>{
{ "Zzzz", Script.Unknown},
{ "Zyyy", Script.Common},
{ "Zinh", Script.Inherited},
{ "Adlm", Script.Adlam},
{ "Aghb", Script.CaucasianAlbanian},
{ "Ahom", Script.Ahom},
{ "Arab", Script.Arabic},
{ "Armi", Script.ImperialAramaic},
{ "Armn", Script.Armenian},
{ "Avst", Script.Avestan},
{ "Bali", Script.Balinese},
{ "Bamu", Script.Bamum},
{ "Bass", Script.BassaVah},
{ "Batk", Script.Batak},
{ "Beng", Script.Bengali},
{ "Bhks", Script.Bhaiksuki},
{ "Bopo", Script.Bopomofo},
{ "Brah", Script.Brahmi},
{ "Brai", Script.Braille},
{ "Bugi", Script.Buginese},
{ "Buhd", Script.Buhid},
{ "Cakm", Script.Chakma},
{ "Cans", Script.CanadianAboriginal},
{ "Cari", Script.Carian},
{ "Cham", Script.Cham},
{ "Cher", Script.Cherokee},
{ "Chrs", Script.Chorasmian},
{ "Copt", Script.Coptic},
{ "Cprt", Script.Cypriot},
{ "Cyrl", Script.Cyrillic},
{ "Deva", Script.Devanagari},
{ "Diak", Script.DivesAkuru},
{ "Dogr", Script.Dogra},
{ "Dsrt", Script.Deseret},
{ "Dupl", Script.Duployan},
{ "Egyp", Script.EgyptianHieroglyphs},
{ "Elba", Script.Elbasan},
{ "Elym", Script.Elymaic},
{ "Ethi", Script.Ethiopic},
{ "Geor", Script.Georgian},
{ "Glag", Script.Glagolitic},
{ "Gong", Script.GunjalaGondi},
{ "Gonm", Script.MasaramGondi},
{ "Goth", Script.Gothic},
{ "Gran", Script.Grantha},
{ "Grek", Script.Greek},
{ "Gujr", Script.Gujarati},
{ "Guru", Script.Gurmukhi},
{ "Hang", Script.Hangul},
{ "Hani", Script.Han},
{ "Hano", Script.Hanunoo},
{ "Hatr", Script.Hatran},
{ "Hebr", Script.Hebrew},
{ "Hira", Script.Hiragana},
{ "Hluw", Script.AnatolianHieroglyphs},
{ "Hmng", Script.PahawhHmong},
{ "Hmnp", Script.NyiakengPuachueHmong},
{ "Hrkt", Script.KatakanaOrHiragana},
{ "Hung", Script.OldHungarian},
{ "Ital", Script.OldItalic},
{ "Java", Script.Javanese},
{ "Kali", Script.KayahLi},
{ "Kana", Script.Katakana},
{ "Khar", Script.Kharoshthi},
{ "Khmr", Script.Khmer},
{ "Khoj", Script.Khojki},
{ "Kits", Script.KhitanSmallScript},
{ "Knda", Script.Kannada},
{ "Kthi", Script.Kaithi},
{ "Lana", Script.TaiTham},
{ "Laoo", Script.Lao},
{ "Latn", Script.Latin},
{ "Lepc", Script.Lepcha},
{ "Limb", Script.Limbu},
{ "Lina", Script.LinearA},
{ "Linb", Script.LinearB},
{ "Lisu", Script.Lisu},
{ "Lyci", Script.Lycian},
{ "Lydi", Script.Lydian},
{ "Mahj", Script.Mahajani},
{ "Maka", Script.Makasar},
{ "Mand", Script.Mandaic},
{ "Mani", Script.Manichaean},
{ "Marc", Script.Marchen},
{ "Medf", Script.Medefaidrin},
{ "Mend", Script.MendeKikakui},
{ "Merc", Script.MeroiticCursive},
{ "Mero", Script.MeroiticHieroglyphs},
{ "Mlym", Script.Malayalam},
{ "Modi", Script.Modi},
{ "Mong", Script.Mongolian},
{ "Mroo", Script.Mro},
{ "Mtei", Script.MeeteiMayek},
{ "Mult", Script.Multani},
{ "Mymr", Script.Myanmar},
{ "Nand", Script.Nandinagari},
{ "Narb", Script.OldNorthArabian},
{ "Nbat", Script.Nabataean},
{ "Newa", Script.Newa},
{ "Nkoo", Script.Nko},
{ "Nshu", Script.Nushu},
{ "Ogam", Script.Ogham},
{ "Olck", Script.OlChiki},
{ "Orkh", Script.OldTurkic},
{ "Orya", Script.Oriya},
{ "Osge", Script.Osage},
{ "Osma", Script.Osmanya},
{ "Palm", Script.Palmyrene},
{ "Pauc", Script.PauCinHau},
{ "Perm", Script.OldPermic},
{ "Phag", Script.PhagsPa},
{ "Phli", Script.InscriptionalPahlavi},
{ "Phlp", Script.PsalterPahlavi},
{ "Phnx", Script.Phoenician},
{ "Plrd", Script.Miao},
{ "Prti", Script.InscriptionalParthian},
{ "Rjng", Script.Rejang},
{ "Rohg", Script.HanifiRohingya},
{ "Runr", Script.Runic},
{ "Samr", Script.Samaritan},
{ "Sarb", Script.OldSouthArabian},
{ "Saur", Script.Saurashtra},
{ "Sgnw", Script.SignWriting},
{ "Shaw", Script.Shavian},
{ "Shrd", Script.Sharada},
{ "Sidd", Script.Siddham},
{ "Sind", Script.Khudawadi},
{ "Sinh", Script.Sinhala},
{ "Sogd", Script.Sogdian},
{ "Sogo", Script.OldSogdian},
{ "Sora", Script.SoraSompeng},
{ "Soyo", Script.Soyombo},
{ "Sund", Script.Sundanese},
{ "Sylo", Script.SylotiNagri},
{ "Syrc", Script.Syriac},
{ "Tagb", Script.Tagbanwa},
{ "Takr", Script.Takri},
{ "Tale", Script.TaiLe},
{ "Talu", Script.NewTaiLue},
{ "Taml", Script.Tamil},
{ "Tang", Script.Tangut},
{ "Tavt", Script.TaiViet},
{ "Telu", Script.Telugu},
{ "Tfng", Script.Tifinagh},
{ "Tglg", Script.Tagalog},
{ "Thaa", Script.Thaana},
{ "Thai", Script.Thai},
{ "Tibt", Script.Tibetan},
{ "Tirh", Script.Tirhuta},
{ "Ugar", Script.Ugaritic},
{ "Vaii", Script.Vai},
{ "Wara", Script.WarangCiti},
{ "Wcho", Script.Wancho},
{ "Xpeo", Script.OldPersian},
{ "Xsux", Script.Cuneiform},
{ "Yezi", Script.Yezidi},
{ "Yiii", Script.Yi},
{ "Zanb", Script.ZanabazarSquare},
};
public static Script GetScript(string tag)
{
if(!s_tagToScript.ContainsKey(tag))
{
return Script.Unknown;
}
return s_tagToScript[tag];
}
private static readonly Dictionary<string, GeneralCategory> s_tagToGeneralCategory =
new Dictionary<string,GeneralCategory>{
{ "C", GeneralCategory.Other},
{ "Cc", GeneralCategory.Control},
{ "Cf", GeneralCategory.Format},
{ "Cn", GeneralCategory.Unassigned},
{ "Co", GeneralCategory.PrivateUse},
{ "Cs", GeneralCategory.Surrogate},
{ "L", GeneralCategory.Letter},
{ "LC", GeneralCategory.CasedLetter},
{ "Ll", GeneralCategory.LowercaseLetter},
{ "Lm", GeneralCategory.ModifierLetter},
{ "Lo", GeneralCategory.OtherLetter},
{ "Lt", GeneralCategory.TitlecaseLetter},
{ "Lu", GeneralCategory.UppercaseLetter},
{ "M", GeneralCategory.Mark},
{ "Mc", GeneralCategory.SpacingMark},
{ "Me", GeneralCategory.EnclosingMark},
{ "Mn", GeneralCategory.NonspacingMark},
{ "N", GeneralCategory.Number},
{ "Nd", GeneralCategory.DecimalNumber},
{ "Nl", GeneralCategory.LetterNumber},
{ "No", GeneralCategory.OtherNumber},
{ "P", GeneralCategory.Punctuation},
{ "Pc", GeneralCategory.ConnectorPunctuation},
{ "Pd", GeneralCategory.DashPunctuation},
{ "Pe", GeneralCategory.ClosePunctuation},
{ "Pf", GeneralCategory.FinalPunctuation},
{ "Pi", GeneralCategory.InitialPunctuation},
{ "Po", GeneralCategory.OtherPunctuation},
{ "Ps", GeneralCategory.OpenPunctuation},
{ "S", GeneralCategory.Symbol},
{ "Sc", GeneralCategory.CurrencySymbol},
{ "Sk", GeneralCategory.ModifierSymbol},
{ "Sm", GeneralCategory.MathSymbol},
{ "So", GeneralCategory.OtherSymbol},
{ "Z", GeneralCategory.Separator},
{ "Zl", GeneralCategory.LineSeparator},
{ "Zp", GeneralCategory.ParagraphSeparator},
{ "Zs", GeneralCategory.SpaceSeparator},
};
public static GeneralCategory GetGeneralCategory(string tag)
{
if(!s_tagToGeneralCategory.ContainsKey(tag))
{
return GeneralCategory.Other;
}
return s_tagToGeneralCategory[tag];
}
private static readonly Dictionary<string, LineBreakClass> s_tagToLineBreakClass =
new Dictionary<string,LineBreakClass>{
{ "OP", LineBreakClass.OpenPunctuation},
{ "CL", LineBreakClass.ClosePunctuation},
{ "CP", LineBreakClass.CloseParenthesis},
{ "QU", LineBreakClass.Quotation},
{ "GL", LineBreakClass.Glue},
{ "NS", LineBreakClass.Nonstarter},
{ "EX", LineBreakClass.Exclamation},
{ "SY", LineBreakClass.BreakSymbols},
{ "IS", LineBreakClass.InfixNumeric},
{ "PR", LineBreakClass.PrefixNumeric},
{ "PO", LineBreakClass.PostfixNumeric},
{ "NU", LineBreakClass.Numeric},
{ "AL", LineBreakClass.Alphabetic},
{ "HL", LineBreakClass.HebrewLetter},
{ "ID", LineBreakClass.Ideographic},
{ "IN", LineBreakClass.Inseparable},
{ "HY", LineBreakClass.Hyphen},
{ "BA", LineBreakClass.BreakAfter},
{ "BB", LineBreakClass.BreakBefore},
{ "B2", LineBreakClass.BreakBoth},
{ "ZW", LineBreakClass.ZWSpace},
{ "CM", LineBreakClass.CombiningMark},
{ "WJ", LineBreakClass.WordJoiner},
{ "H2", LineBreakClass.H2},
{ "H3", LineBreakClass.H3},
{ "JL", LineBreakClass.JL},
{ "JV", LineBreakClass.JV},
{ "JT", LineBreakClass.JT},
{ "RI", LineBreakClass.RegionalIndicator},
{ "EB", LineBreakClass.EBase},
{ "EM", LineBreakClass.EModifier},
{ "ZWJ", LineBreakClass.ZWJ},
{ "CB", LineBreakClass.ContingentBreak},
{ "XX", LineBreakClass.Unknown},
{ "AI", LineBreakClass.Ambiguous},
{ "BK", LineBreakClass.MandatoryBreak},
{ "CJ", LineBreakClass.ConditionalJapaneseStarter},
{ "CR", LineBreakClass.CarriageReturn},
{ "LF", LineBreakClass.LineFeed},
{ "NL", LineBreakClass.NextLine},
{ "SA", LineBreakClass.ComplexContext},
{ "SG", LineBreakClass.Surrogate},
{ "SP", LineBreakClass.Space},
};
public static LineBreakClass GetLineBreakClass(string tag)
{
if(!s_tagToLineBreakClass.ContainsKey(tag))
{
return LineBreakClass.Unknown;
}
return s_tagToLineBreakClass[tag];
}
private static readonly Dictionary<string, BidiPairedBracketType> s_tagToBiDiPairedBracketType =
new Dictionary<string,BidiPairedBracketType>{
{ "n", BidiPairedBracketType.None},
{ "c", BidiPairedBracketType.Close},
{ "o", BidiPairedBracketType.Open},
};
public static BidiPairedBracketType GetBiDiPairedBracketType(string tag)
{
if(!s_tagToBiDiPairedBracketType.ContainsKey(tag))
{
return BidiPairedBracketType.None;
}
return s_tagToBiDiPairedBracketType[tag];
}
private static readonly Dictionary<string, BidiClass> s_tagToBiDiClass =
new Dictionary<string,BidiClass>{
{ "L", BidiClass.LeftToRight},
{ "AL", BidiClass.ArabicLetter},
{ "AN", BidiClass.ArabicNumber},
{ "B", BidiClass.ParagraphSeparator},
{ "BN", BidiClass.BoundaryNeutral},
{ "CS", BidiClass.CommonSeparator},
{ "EN", BidiClass.EuropeanNumber},
{ "ES", BidiClass.EuropeanSeparator},
{ "ET", BidiClass.EuropeanTerminator},
{ "FSI", BidiClass.FirstStrongIsolate},
{ "LRE", BidiClass.LeftToRightEmbedding},
{ "LRI", BidiClass.LeftToRightIsolate},
{ "LRO", BidiClass.LeftToRightOverride},
{ "NSM", BidiClass.NonspacingMark},
{ "ON", BidiClass.OtherNeutral},
{ "PDF", BidiClass.PopDirectionalFormat},
{ "PDI", BidiClass.PopDirectionalIsolate},
{ "R", BidiClass.RightToLeft},
{ "RLE", BidiClass.RightToLeftEmbedding},
{ "RLI", BidiClass.RightToLeftIsolate},
{ "RLO", BidiClass.RightToLeftOverride},
{ "S", BidiClass.SegmentSeparator},
{ "WS", BidiClass.WhiteSpace},
};
public static BidiClass GetBiDiClass(string tag)
{
if(!s_tagToBiDiClass.ContainsKey(tag))
{
return BidiClass.LeftToRight;
}
return s_tagToBiDiClass[tag];
}
} }
} }

71
src/Avalonia.Visuals/Media/TextFormatting/Unicode/UnicodeData.cs

@ -1,4 +1,6 @@
namespace Avalonia.Media.TextFormatting.Unicode using System.Runtime.CompilerServices;
namespace Avalonia.Media.TextFormatting.Unicode
{ {
/// <summary> /// <summary>
/// Helper for looking up unicode character class information /// Helper for looking up unicode character class information
@ -7,25 +9,35 @@
{ {
internal const int CATEGORY_BITS = 6; internal const int CATEGORY_BITS = 6;
internal const int SCRIPT_BITS = 8; internal const int SCRIPT_BITS = 8;
internal const int BIDI_BITS = 5;
internal const int LINEBREAK_BITS = 6; internal const int LINEBREAK_BITS = 6;
internal const int SCRIPT_SHIFT = CATEGORY_BITS; internal const int BIDIPAIREDBRACKED_BITS = 16;
internal const int BIDI_SHIFT = CATEGORY_BITS + SCRIPT_BITS; internal const int BIDIPAIREDBRACKEDTYPE_BITS = 2;
internal const int LINEBREAK_SHIFT = CATEGORY_BITS + SCRIPT_BITS + BIDI_BITS; internal const int BIDICLASS_BITS = 5;
internal const int SCRIPT_SHIFT = CATEGORY_BITS;
internal const int LINEBREAK_SHIFT = CATEGORY_BITS + SCRIPT_BITS;
internal const int BIDIPAIREDBRACKEDTYPE_SHIFT = BIDIPAIREDBRACKED_BITS;
internal const int BIDICLASS_SHIFT = BIDIPAIREDBRACKED_BITS + BIDIPAIREDBRACKEDTYPE_BITS;
internal const int CATEGORY_MASK = (1 << CATEGORY_BITS) - 1; internal const int CATEGORY_MASK = (1 << CATEGORY_BITS) - 1;
internal const int SCRIPT_MASK = (1 << SCRIPT_BITS) - 1; internal const int SCRIPT_MASK = (1 << SCRIPT_BITS) - 1;
internal const int BIDI_MASK = (1 << BIDI_BITS) - 1;
internal const int LINEBREAK_MASK = (1 << LINEBREAK_BITS) - 1; internal const int LINEBREAK_MASK = (1 << LINEBREAK_BITS) - 1;
internal const int BIDIPAIREDBRACKED_MASK = (1 << BIDIPAIREDBRACKED_BITS) - 1;
internal const int BIDIPAIREDBRACKEDTYPE_MASK = (1 << BIDIPAIREDBRACKEDTYPE_BITS) - 1;
internal const int BIDICLASS_MASK = (1 << BIDICLASS_BITS) - 1;
private static readonly UnicodeTrie s_unicodeDataTrie; private static readonly UnicodeTrie s_unicodeDataTrie;
private static readonly UnicodeTrie s_graphemeBreakTrie; private static readonly UnicodeTrie s_graphemeBreakTrie;
private static readonly UnicodeTrie s_biDiTrie;
static UnicodeData() static UnicodeData()
{ {
s_unicodeDataTrie = new UnicodeTrie(typeof(UnicodeData).Assembly.GetManifestResourceStream("Avalonia.Assets.UnicodeData.trie")!); s_unicodeDataTrie = new UnicodeTrie(typeof(UnicodeData).Assembly.GetManifestResourceStream("Avalonia.Assets.UnicodeData.trie")!);
s_graphemeBreakTrie = new UnicodeTrie(typeof(UnicodeData).Assembly.GetManifestResourceStream("Avalonia.Assets.GraphemeBreak.trie")!); s_graphemeBreakTrie = new UnicodeTrie(typeof(UnicodeData).Assembly.GetManifestResourceStream("Avalonia.Assets.GraphemeBreak.trie")!);
s_biDiTrie = new UnicodeTrie(typeof(UnicodeData).Assembly.GetManifestResourceStream("Avalonia.Assets.BiDi.trie")!);
} }
/// <summary> /// <summary>
@ -33,11 +45,10 @@
/// </summary> /// </summary>
/// <param name="codepoint">The codepoint in question.</param> /// <param name="codepoint">The codepoint in question.</param>
/// <returns>The code point's general category.</returns> /// <returns>The code point's general category.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static GeneralCategory GetGeneralCategory(int codepoint) public static GeneralCategory GetGeneralCategory(int codepoint)
{ {
var value = s_unicodeDataTrie.Get(codepoint); return (GeneralCategory)(s_unicodeDataTrie.Get(codepoint) & CATEGORY_MASK);
return (GeneralCategory)(value & CATEGORY_MASK);
} }
/// <summary> /// <summary>
@ -45,23 +56,43 @@
/// </summary> /// </summary>
/// <param name="codepoint">The codepoint in question.</param> /// <param name="codepoint">The codepoint in question.</param>
/// <returns>The code point's script.</returns> /// <returns>The code point's script.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Script GetScript(int codepoint) public static Script GetScript(int codepoint)
{ {
var value = s_unicodeDataTrie.Get(codepoint); return (Script)((s_unicodeDataTrie.Get(codepoint) >> SCRIPT_SHIFT) & SCRIPT_MASK);
return (Script)((value >> SCRIPT_SHIFT) & SCRIPT_MASK);
} }
/// <summary> /// <summary>
/// Gets the <see cref="BiDiClass"/> for a Unicode codepoint. /// Gets the <see cref="BidiClass"/> for a Unicode codepoint.
/// </summary> /// </summary>
/// <param name="codepoint">The codepoint in question.</param> /// <param name="codepoint">The codepoint in question.</param>
/// <returns>The code point's biDi class.</returns> /// <returns>The code point's biDi class.</returns>
public static BiDiClass GetBiDiClass(int codepoint) [MethodImpl(MethodImplOptions.AggressiveInlining)]
public static BidiClass GetBiDiClass(int codepoint)
{ {
var value = s_unicodeDataTrie.Get(codepoint); return (BidiClass)((s_biDiTrie.Get(codepoint) >> BIDICLASS_SHIFT) & BIDICLASS_MASK);
}
return (BiDiClass)((value >> BIDI_SHIFT) & BIDI_MASK);
/// <summary>
/// Gets the <see cref="BidiPairedBracketType"/> for a Unicode codepoint.
/// </summary>
/// <param name="codepoint">The codepoint in question.</param>
/// <returns>The code point's paired bracket type.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static BidiPairedBracketType GetBiDiPairedBracketType(int codepoint)
{
return (BidiPairedBracketType)((s_biDiTrie.Get(codepoint) >> BIDIPAIREDBRACKEDTYPE_SHIFT) & BIDIPAIREDBRACKEDTYPE_MASK);
}
/// <summary>
/// Gets the paired bracket for a Unicode codepoint.
/// </summary>
/// <param name="codepoint">The codepoint in question.</param>
/// <returns>The code point's paired bracket.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Codepoint GetBiDiPairedBracket(int codepoint)
{
return new Codepoint((int)(s_biDiTrie.Get(codepoint) & BIDIPAIREDBRACKED_MASK));
} }
/// <summary> /// <summary>
@ -69,11 +100,10 @@
/// </summary> /// </summary>
/// <param name="codepoint">The codepoint in question.</param> /// <param name="codepoint">The codepoint in question.</param>
/// <returns>The code point's line break class.</returns> /// <returns>The code point's line break class.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static LineBreakClass GetLineBreakClass(int codepoint) public static LineBreakClass GetLineBreakClass(int codepoint)
{ {
var value = s_unicodeDataTrie.Get(codepoint); return (LineBreakClass)((s_unicodeDataTrie.Get(codepoint) >> LINEBREAK_SHIFT) & LINEBREAK_MASK);
return (LineBreakClass)((value >> LINEBREAK_SHIFT) & LINEBREAK_MASK);
} }
/// <summary> /// <summary>
@ -81,6 +111,7 @@
/// </summary> /// </summary>
/// <param name="codepoint">The codepoint in question.</param> /// <param name="codepoint">The codepoint in question.</param>
/// <returns>The code point's grapheme break type.</returns> /// <returns>The code point's grapheme break type.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static GraphemeBreakClass GetGraphemeClusterBreak(int codepoint) public static GraphemeBreakClass GetGraphemeClusterBreak(int codepoint)
{ {
return (GraphemeBreakClass)s_graphemeBreakTrie.Get(codepoint); return (GraphemeBreakClass)s_graphemeBreakTrie.Get(codepoint);

27
src/Avalonia.Visuals/Media/TextHitTestResult.cs

@ -1,23 +1,38 @@
using Avalonia.Media.TextFormatting;
namespace Avalonia.Media namespace Avalonia.Media
{ {
/// <summary> /// <summary>
/// Holds a hit test result from a <see cref="FormattedText"/>. /// Holds a hit test result from a <see cref="TextLayout"/>.
/// </summary> /// </summary>
public class TextHitTestResult public readonly struct TextHitTestResult
{ {
public TextHitTestResult(CharacterHit characterHit, int textPosition, bool isInside, bool isTrailing)
{
CharacterHit = characterHit;
TextPosition = textPosition;
IsInside = isInside;
IsTrailing = isTrailing;
}
/// <summary>
/// Gets the character hit of the hit test result.
/// </summary>
public CharacterHit CharacterHit { get; }
/// <summary> /// <summary>
/// Gets or sets a value indicating whether the point is inside the bounds of the text. /// Gets a value indicating whether the point is inside the bounds of the text.
/// </summary> /// </summary>
public bool IsInside { get; set; } public bool IsInside { get; }
/// <summary> /// <summary>
/// Gets the index of the hit character in the text. /// Gets the index of the hit character in the text.
/// </summary> /// </summary>
public int TextPosition { get; set; } public int TextPosition { get; }
/// <summary> /// <summary>
/// Gets a value indicating whether the hit is on the trailing edge of the character. /// Gets a value indicating whether the hit is on the trailing edge of the character.
/// </summary> /// </summary>
public bool IsTrailing { get; set; } public bool IsTrailing { get; }
} }
} }

2
src/Avalonia.Visuals/Media/Typeface.cs

@ -48,7 +48,7 @@ namespace Avalonia.Media
/// <summary> /// <summary>
/// Gets the font family. /// Gets the font family.
/// </summary> /// </summary>
public FontFamily? FontFamily { get; } public FontFamily FontFamily { get; }
/// <summary> /// <summary>
/// Gets the font style. /// Gets the font style.

7
src/Avalonia.Visuals/Platform/IDrawingContextImpl.cs

@ -83,13 +83,6 @@ namespace Avalonia.Platform
/// </remarks> /// </remarks>
void DrawEllipse(IBrush? brush, IPen? pen, Rect rect); void DrawEllipse(IBrush? brush, IPen? pen, Rect rect);
/// <summary>
/// Draws text.
/// </summary>
/// <param name="foreground">The foreground brush.</param>
/// <param name="origin">The upper-left corner of the text.</param>
/// <param name="text">The text.</param>
void DrawText(IBrush foreground, Point origin, IFormattedTextImpl text);
/// <summary> /// <summary>
/// Draws a glyph run. /// Draws a glyph run.

59
src/Avalonia.Visuals/Platform/IFormattedTextImpl.cs

@ -1,59 +0,0 @@
using System;
using System.Collections.Generic;
using Avalonia.Media;
namespace Avalonia.Platform
{
/// <summary>
/// Defines the platform-specific interface for <see cref="FormattedText"/>.
/// </summary>
public interface IFormattedTextImpl
{
/// <summary>
/// Gets the constraint of the text.
/// </summary>
Size Constraint { get; }
/// <summary>
/// The measured bounds of the text.
/// </summary>
Rect Bounds{ get; }
/// <summary>
/// Gets the text.
/// </summary>
string Text { get; }
/// <summary>
/// Gets the lines in the text.
/// </summary>
/// <returns>
/// A collection of <see cref="FormattedTextLine"/> objects.
/// </returns>
IEnumerable<FormattedTextLine> GetLines();
/// <summary>
/// Hit tests a point in the text.
/// </summary>
/// <param name="point">The point.</param>
/// <returns>
/// A <see cref="TextHitTestResult"/> describing the result of the hit test.
/// </returns>
TextHitTestResult HitTestPoint(Point point);
/// <summary>
/// Gets the bounds rectangle that the specified character occupies.
/// </summary>
/// <param name="index">The index of the character.</param>
/// <returns>The character bounds.</returns>
Rect HitTestTextPosition(int index);
/// <summary>
/// Gets the bounds rectangles that the specified text range occupies.
/// </summary>
/// <param name="index">The index of the first character.</param>
/// <param name="length">The number of characters in the text range.</param>
/// <returns>The character bounds.</returns>
IEnumerable<Rect> HitTestTextRange(int index, int length);
}
}

21
src/Avalonia.Visuals/Platform/IPlatformRenderInterface.cs

@ -11,27 +11,6 @@ namespace Avalonia.Platform
/// </summary> /// </summary>
public interface IPlatformRenderInterface public interface IPlatformRenderInterface
{ {
/// <summary>
/// Creates a formatted text implementation.
/// </summary>
/// <param name="text">The text.</param>
/// <param name="typeface">The base typeface.</param>
/// <param name="fontSize">The font size.</param>
/// <param name="textAlignment">The text alignment.</param>
/// <param name="wrapping">The text wrapping mode.</param>
/// <param name="constraint">The text layout constraints.</param>
/// <param name="spans">The style spans.</param>
/// <returns>An <see cref="IFormattedTextImpl"/>.</returns>
IFormattedTextImpl CreateFormattedText(
string text,
Typeface typeface,
double fontSize,
TextAlignment textAlignment,
TextWrapping wrapping,
Size constraint,
IReadOnlyList<FormattedTextStyleSpan>? spans);
/// <summary>
/// Creates an ellipse geometry implementation. /// Creates an ellipse geometry implementation.
/// </summary> /// </summary>
/// <param name="rect">The bounds of the ellipse.</param> /// <param name="rect">The bounds of the ellipse.</param>

6
src/Avalonia.Visuals/Platform/ITextShaperImpl.cs

@ -1,5 +1,6 @@
using System.Globalization; using System.Globalization;
using Avalonia.Media; using Avalonia.Media;
using Avalonia.Media.TextFormatting;
using Avalonia.Utilities; using Avalonia.Utilities;
namespace Avalonia.Platform namespace Avalonia.Platform
@ -10,13 +11,14 @@ namespace Avalonia.Platform
public interface ITextShaperImpl public interface ITextShaperImpl
{ {
/// <summary> /// <summary>
/// Shapes the specified region within the text and returns a resulting glyph run. /// Shapes the specified region within the text and returns a shaped buffer.
/// </summary> /// </summary>
/// <param name="text">The text.</param> /// <param name="text">The text.</param>
/// <param name="typeface">The typeface.</param> /// <param name="typeface">The typeface.</param>
/// <param name="fontRenderingEmSize">The font rendering em size.</param> /// <param name="fontRenderingEmSize">The font rendering em size.</param>
/// <param name="culture">The culture.</param> /// <param name="culture">The culture.</param>
/// <param name="bidiLevel">The bidi level.</param>
/// <returns>A shaped glyph run.</returns> /// <returns>A shaped glyph run.</returns>
GlyphRun ShapeText(ReadOnlySlice<char> text, Typeface typeface, double fontRenderingEmSize, CultureInfo? culture); ShapedBuffer ShapeText(ReadOnlySlice<char> text, GlyphTypeface typeface, double fontRenderingEmSize, CultureInfo? culture, sbyte bidiLevel);
} }
} }

5
src/Avalonia.Visuals/Rendering/DeferredRenderer.cs

@ -588,7 +588,10 @@ namespace Avalonia.Rendering
if (DrawFps) if (DrawFps)
{ {
RenderFps(context, clientRect, scene.Layers.Count); using (var c = new DrawingContext(context, false))
{
RenderFps(c, clientRect, scene.Layers.Count);
}
} }
} }

2
src/Avalonia.Visuals/Rendering/ImmediateRenderer.cs

@ -78,7 +78,7 @@ namespace Avalonia.Rendering
if (DrawFps) if (DrawFps)
{ {
RenderFps(context.PlatformImpl, _root.Bounds, null); RenderFps(context, _root.Bounds, null);
} }
} }
} }

30
src/Avalonia.Visuals/Rendering/RendererBase.cs

@ -1,7 +1,7 @@
using System; using System;
using System.Diagnostics; using System.Diagnostics;
using System.Globalization;
using Avalonia.Media; using Avalonia.Media;
using Avalonia.Platform;
namespace Avalonia.Rendering namespace Avalonia.Rendering
{ {
@ -12,22 +12,16 @@ namespace Avalonia.Rendering
private readonly Stopwatch _stopwatch = Stopwatch.StartNew(); private readonly Stopwatch _stopwatch = Stopwatch.StartNew();
private int _framesThisSecond; private int _framesThisSecond;
private int _fps; private int _fps;
private FormattedText _fpsText;
private TimeSpan _lastFpsUpdate; private TimeSpan _lastFpsUpdate;
public RendererBase(bool useManualFpsCounting = false) public RendererBase(bool useManualFpsCounting = false)
{ {
_useManualFpsCounting = useManualFpsCounting; _useManualFpsCounting = useManualFpsCounting;
_fpsText = new FormattedText
{
Typeface = new Typeface(FontFamily.Default),
FontSize = s_fontSize
};
} }
protected void FpsTick() => _framesThisSecond++; protected void FpsTick() => _framesThisSecond++;
protected void RenderFps(IDrawingContextImpl context, Rect clientRect, int? layerCount) protected void RenderFps(DrawingContext context, Rect clientRect, int? layerCount)
{ {
var now = _stopwatch.Elapsed; var now = _stopwatch.Elapsed;
var elapsed = now - _lastFpsUpdate; var elapsed = now - _lastFpsUpdate;
@ -42,21 +36,15 @@ namespace Avalonia.Rendering
_lastFpsUpdate = now; _lastFpsUpdate = now;
} }
if (layerCount.HasValue) var text = layerCount.HasValue ? $"Layers: {layerCount} FPS: {_fps:000}" : $"FPS: {_fps:000}";
{
_fpsText.Text = string.Format("Layers: {0} FPS: {1:000}", layerCount, _fps); var formattedText = new FormattedText(text, CultureInfo.CurrentCulture, FlowDirection.LeftToRight, Typeface.Default, s_fontSize, Brushes.White);
}
else var rect = new Rect(clientRect.Right - formattedText.Width, 0, formattedText.Width, formattedText.Height);
{
_fpsText.Text = string.Format("FPS: {0:000}", _fps);
}
var size = _fpsText.Bounds.Size; context.DrawRectangle(Brushes.Black, null, rect);
var rect = new Rect(clientRect.Right - size.Width, 0, size.Width, size.Height);
context.Transform = Matrix.Identity; context.DrawText(formattedText, rect.TopLeft);
context.DrawRectangle(Brushes.Black,null, rect);
context.DrawText(Brushes.White, rect.TopLeft, _fpsText.PlatformImpl);
} }
} }
} }

15
src/Avalonia.Visuals/Rendering/SceneGraph/DeferredDrawingContextImpl.cs

@ -202,21 +202,6 @@ namespace Avalonia.Rendering.SceneGraph
++_drawOperationindex; ++_drawOperationindex;
} }
/// <inheritdoc/>
public void DrawText(IBrush foreground, Point origin, IFormattedTextImpl text)
{
var next = NextDrawAs<TextNode>();
if (next == null || !next.Item.Equals(Transform, foreground, origin, text))
{
Add(new TextNode(Transform, foreground, origin, text, CreateChildScene(foreground)));
}
else
{
++_drawOperationindex;
}
}
/// <inheritdoc/> /// <inheritdoc/>
public void DrawGlyphRun(IBrush foreground, GlyphRun glyphRun) public void DrawGlyphRun(IBrush foreground, GlyphRun glyphRun)
{ {

89
src/Avalonia.Visuals/Rendering/SceneGraph/TextNode.cs

@ -1,89 +0,0 @@
using System.Collections.Generic;
using Avalonia.Media;
using Avalonia.Platform;
using Avalonia.VisualTree;
namespace Avalonia.Rendering.SceneGraph
{
/// <summary>
/// A node in the scene graph which represents a text draw.
/// </summary>
internal class TextNode : BrushDrawOperation
{
/// <summary>
/// Initializes a new instance of the <see cref="TextNode"/> class.
/// </summary>
/// <param name="transform">The transform.</param>
/// <param name="foreground">The foreground brush.</param>
/// <param name="origin">The draw origin.</param>
/// <param name="text">The text to draw.</param>
/// <param name="childScenes">Child scenes for drawing visual brushes.</param>
public TextNode(
Matrix transform,
IBrush foreground,
Point origin,
IFormattedTextImpl text,
IDictionary<IVisual, Scene>? childScenes = null)
: base(text.Bounds.Translate(origin), transform)
{
Transform = transform;
Foreground = foreground.ToImmutable();
Origin = origin;
Text = text;
ChildScenes = childScenes;
}
/// <summary>
/// Gets the transform with which the node will be drawn.
/// </summary>
public Matrix Transform { get; }
/// <summary>
/// Gets the foreground brush.
/// </summary>
public IBrush Foreground { get; }
/// <summary>
/// Gets the draw origin.
/// </summary>
public Point Origin { get; }
/// <summary>
/// Gets the text to draw.
/// </summary>
public IFormattedTextImpl Text { get; }
/// <inheritdoc/>
public override IDictionary<IVisual, Scene>? ChildScenes { get; }
/// <inheritdoc/>
public override void Render(IDrawingContextImpl context)
{
context.Transform = Transform;
context.DrawText(Foreground, Origin, Text);
}
/// <summary>
/// Determines if this draw operation equals another.
/// </summary>
/// <param name="transform">The transform of the other draw operation.</param>
/// <param name="foreground">The foreground of the other draw operation.</param>
/// <param name="origin">The draw origin of the other draw operation.</param>
/// <param name="text">The text of the other draw operation.</param>
/// <returns>True if the draw operations are the same, otherwise false.</returns>
/// <remarks>
/// The properties of the other draw operation are passed in as arguments to prevent
/// allocation of a not-yet-constructed draw operation object.
/// </remarks>
internal bool Equals(Matrix transform, IBrush foreground, Point origin, IFormattedTextImpl text)
{
return transform == Transform &&
Equals(foreground, Foreground) &&
origin == Origin &&
Equals(text, Text);
}
/// <inheritdoc/>
public override bool HitTest(Point p) => Bounds.Contains(p);
}
}

184
src/Avalonia.Visuals/Utilities/ArrayBuilder.cs

@ -0,0 +1,184 @@
// Copyright (c) Six Labors.
// Licensed under the Apache License, Version 2.0.
// Ported from: https://github.com/SixLabors/Fonts/
using System;
using System.Runtime.CompilerServices;
namespace Avalonia.Utilities
{
/// <summary>
/// A helper type for avoiding allocations while building arrays.
/// </summary>
/// <typeparam name="T">The type of item contained in the array.</typeparam>
internal struct ArrayBuilder<T>
where T : struct
{
private const int DefaultCapacity = 4;
private const int MaxCoreClrArrayLength = 0x7FeFFFFF;
// Starts out null, initialized on first Add.
private T[] _data;
private int _size;
/// <summary>
/// Gets or sets the number of items in the array.
/// </summary>
public int Length
{
get => _size;
set
{
if (value == _size)
{
return;
}
if (value > 0)
{
EnsureCapacity(value);
_size = value;
}
else
{
_size = 0;
}
}
}
/// <summary>
/// Returns a reference to specified element of the array.
/// </summary>
/// <param name="index">The index of the element to return.</param>
/// <returns>The <typeparamref name="T"/>.</returns>
/// <exception cref="IndexOutOfRangeException">
/// Thrown when index less than 0 or index greater than or equal to <see cref="Length"/>.
/// </exception>
public ref T this[int index]
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get
{
#if DEBUG
if (index.CompareTo(0) < 0 || index.CompareTo(_size) > 0)
{
throw new ArgumentOutOfRangeException(nameof(index));
}
#endif
return ref _data![index];
}
}
/// <summary>
/// Appends a given number of empty items to the array returning
/// the items as a slice.
/// </summary>
/// <param name="length">The number of items in the slice.</param>
/// <param name="clear">Whether to clear the new slice, Defaults to <see langword="true"/>.</param>
/// <returns>The <see cref="ArraySlice{T}"/>.</returns>
public ArraySlice<T> Add(int length, bool clear = true)
{
var position = _size;
// Expand the array.
Length += length;
var slice = AsSlice(position, Length - position);
if (clear)
{
slice.Span.Clear();
}
return slice;
}
/// <summary>
/// Appends the slice to the array copying the data across.
/// </summary>
/// <param name="value">The array slice.</param>
/// <returns>The <see cref="ArraySlice{T}"/>.</returns>
public ArraySlice<T> Add(in ReadOnlySlice<T> value)
{
var position = _size;
// Expand the array.
Length += value.Length;
var slice = AsSlice(position, Length - position);
value.Span.CopyTo(slice.Span);
return slice;
}
/// <summary>
/// Clears the array.
/// Allocated memory is left intact for future usage.
/// </summary>
public void Clear()
{
// No need to actually clear since we're not allowing reference types.
_size = 0;
}
private void EnsureCapacity(int min)
{
var length = _data?.Length ?? 0;
if (length >= min)
{
return;
}
// Same expansion algorithm as List<T>.
var newCapacity = length == 0 ? DefaultCapacity : (uint)length * 2u;
if (newCapacity > MaxCoreClrArrayLength)
{
newCapacity = MaxCoreClrArrayLength;
}
if (newCapacity < min)
{
newCapacity = (uint)min;
}
var array = new T[newCapacity];
if (_size > 0)
{
Array.Copy(_data!, array, _size);
}
_data = array;
}
/// <summary>
/// Returns the current state of the array as a slice.
/// </summary>
/// <returns>The <see cref="ArraySlice{T}"/>.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public ArraySlice<T> AsSlice() => AsSlice(Length);
/// <summary>
/// Returns the current state of the array as a slice.
/// </summary>
/// <param name="length">The number of items in the slice.</param>
/// <returns>The <see cref="ArraySlice{T}"/>.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public ArraySlice<T> AsSlice(int length) => new ArraySlice<T>(_data!, 0, length);
/// <summary>
/// Returns the current state of the array as a slice.
/// </summary>
/// <param name="start">The index at which to begin the slice.</param>
/// <param name="length">The number of items in the slice.</param>
/// <returns>The <see cref="ArraySlice{T}"/>.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public ArraySlice<T> AsSlice(int start, int length) => new ArraySlice<T>(_data!, start, length);
}
}

197
src/Avalonia.Visuals/Utilities/ArraySlice.cs

@ -0,0 +1,197 @@
// Copyright (c) Six Labors.
// Licensed under the Apache License, Version 2.0.
// Ported from: https://github.com/SixLabors/Fonts/
using System;
using System.Collections;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
namespace Avalonia.Utilities
{
/// <summary>
/// ArraySlice represents a contiguous region of arbitrary memory similar
/// to <see cref="Memory{T}"/> and <see cref="Span{T}"/> though constrained
/// to arrays.
/// Unlike <see cref="Span{T}"/>, it is not a byref-like type.
/// </summary>
/// <typeparam name="T">The type of item contained in the slice.</typeparam>
internal readonly struct ArraySlice<T> : IReadOnlyList<T>
where T : struct
{
/// <summary>
/// Gets an empty <see cref="ArraySlice{T}"/>
/// </summary>
public static ArraySlice<T> Empty => new ArraySlice<T>(Array.Empty<T>());
private readonly T[] _data;
/// <summary>
/// Initializes a new instance of the <see cref="ArraySlice{T}"/> struct.
/// </summary>
/// <param name="data">The underlying data buffer.</param>
public ArraySlice(T[] data)
: this(data, 0, data.Length)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="ArraySlice{T}"/> struct.
/// </summary>
/// <param name="data">The underlying data buffer.</param>
/// <param name="start">The offset position in the underlying buffer this slice was created from.</param>
/// <param name="length">The number of items in the slice.</param>
public ArraySlice(T[] data, int start, int length)
{
#if DEBUG
if (start.CompareTo(0) < 0)
{
throw new ArgumentOutOfRangeException(nameof(start));
}
if (length.CompareTo(data.Length) > 0)
{
throw new ArgumentOutOfRangeException(nameof(length));
}
if ((start + length).CompareTo(data.Length) > 0)
{
throw new ArgumentOutOfRangeException(nameof(data));
}
#endif
_data = data;
Start = start;
Length = length;
}
/// <summary>
/// Gets a value that indicates whether this instance of <see cref="ArraySlice{T}"/> is Empty.
/// </summary>
public bool IsEmpty => Length == 0;
/// <summary>
/// Gets the offset position in the underlying buffer this slice was created from.
/// </summary>
public int Start { get; }
/// <summary>
/// Gets the number of items in the slice.
/// </summary>
public int Length { get; }
/// <summary>
/// Gets a <see cref="Span{T}"/> representing this slice.
/// </summary>
public Span<T> Span => new Span<T>(_data, Start, Length);
/// <summary>
/// Returns a reference to specified element of the slice.
/// </summary>
/// <param name="index">The index of the element to return.</param>
/// <returns>The <typeparamref name="T"/>.</returns>
/// <exception cref="IndexOutOfRangeException">
/// Thrown when index less than 0 or index greater than or equal to <see cref="Length"/>.
/// </exception>
public ref T this[int index]
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get
{
#if DEBUG
if (index.CompareTo(0) < 0 || index.CompareTo(Length) > 0)
{
throw new ArgumentOutOfRangeException(nameof(index));
}
#endif
var i = index + Start;
return ref _data[i];
}
}
/// <summary>
/// Defines an implicit conversion of a <see cref="ArraySlice{T}"/> to a <see cref="ReadOnlySlice{T}"/>
/// </summary>
public static implicit operator ReadOnlySlice<T>(ArraySlice<T> slice)
{
return new ReadOnlySlice<T>(slice._data).AsSlice(slice.Start, slice.Length);
}
/// <summary>
/// Defines an implicit conversion of an array to a <see cref="ArraySlice{T}"/>
/// </summary>
public static implicit operator ArraySlice<T>(T[] array) => new ArraySlice<T>(array, 0, array.Length);
/// <summary>
/// Fills the contents of this slice with the given value.
/// </summary>
public void Fill(T value) => Span.Fill(value);
/// <summary>
/// Forms a slice out of the given slice, beginning at 'start', of given length
/// </summary>
/// <param name="start">The index at which to begin this slice.</param>
/// <param name="length">The desired length for the slice (exclusive).</param>
/// <exception cref="ArgumentOutOfRangeException">
/// Thrown when the specified <paramref name="start"/> or end index is not in range (&lt;0 or &gt;Length).
/// </exception>
public ArraySlice<T> Slice(int start, int length) => new ArraySlice<T>(_data, start, length);
/// <summary>
/// Returns a specified number of contiguous elements from the start of the slice.
/// </summary>
/// <param name="length">The number of elements to return.</param>
/// <returns>A <see cref="ArraySlice{T}"/> that contains the specified number of elements from the start of this slice.</returns>
public ArraySlice<T> Take(int length)
{
if (IsEmpty)
{
return this;
}
if (length > Length)
{
throw new ArgumentOutOfRangeException(nameof(length));
}
return new ArraySlice<T>(_data, Start, length);
}
/// <summary>
/// Bypasses a specified number of elements in the slice and then returns the remaining elements.
/// </summary>
/// <param name="length">The number of elements to skip before returning the remaining elements.</param>
/// <returns>A <see cref="ArraySlice{T}"/> that contains the elements that occur after the specified index in this slice.</returns>
public ArraySlice<T> Skip(int length)
{
if (IsEmpty)
{
return this;
}
if (length > Length)
{
throw new ArgumentOutOfRangeException(nameof(length));
}
return new ArraySlice<T>(_data, Start + length, Length - length);
}
public ImmutableReadOnlyListStructEnumerator<T> GetEnumerator() =>
new ImmutableReadOnlyListStructEnumerator<T>(this);
/// <inheritdoc/>
IEnumerator<T> IEnumerable<T>.GetEnumerator() => GetEnumerator();
/// <inheritdoc/>
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
/// <inheritdoc/>
T IReadOnlyList<T>.this[int index] => this[index];
/// <inheritdoc/>
int IReadOnlyCollection<T>.Count => Length;
}
}

80
src/Avalonia.Visuals/Utilities/BinarySearchExtension.cs

@ -0,0 +1,80 @@
// RichTextKit
// Copyright © 2019-2020 Topten Software. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License"); you may
// not use this product except in compliance with the License. You may obtain
// a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
// License for the specific language governing permissions and limitations
// under the License.
// Copied from: https://github.com/toptensoftware/RichTextKit
using System;
using System.Collections.Generic;
namespace Avalonia.Utilities
{
/// <summary>
/// Extension methods for binary searching an IReadOnlyList collection
/// </summary>
internal static class BinarySearchExtension
{
private static int GetMedian(int low, int hi)
{
System.Diagnostics.Debug.Assert(low <= hi);
System.Diagnostics.Debug.Assert(hi - low >= 0, "Length overflow!");
return low + (hi - low >> 1);
}
/// <summary>
/// Performs a binary search on the entire contents of an IReadOnlyList
/// </summary>
/// <typeparam name="T">The list element type</typeparam>
/// <param name="list">The list to be searched</param>
/// <param name="value">The value to search for</param>
/// <param name="comparer">The comparer</param>
/// <returns>The index of the found item; otherwise the bitwise complement of the index of the next larger item</returns>
public static int BinarySearch<T>(this IReadOnlyList<T> list, T value, IComparer<T> comparer) where T : IComparable
{
return list.BinarySearch(0, list.Count, value, comparer);
}
/// <summary>
/// Performs a binary search on a a subset of an IReadOnlyList
/// </summary>
/// <typeparam name="T">The list element type</typeparam>
/// <param name="list">The list to be searched</param>
/// <param name="index">The start of the range to be searched</param>
/// <param name="length">The length of the range to be searched</param>
/// <param name="value">The value to search for</param>
/// <param name="comparer">A comparer</param>
/// <returns>The index of the found item; otherwise the bitwise complement of the index of the next larger item</returns>
public static int BinarySearch<T>(this IReadOnlyList<T> list, int index, int length, T value, IComparer<T> comparer)
{
// Based on this: https://referencesource.microsoft.com/#mscorlib/system/array.cs,957
var lo = index;
var hi = index + length - 1;
while (lo <= hi)
{
var i = GetMedian(lo, hi);
var c = comparer.Compare(list[i], value);
if (c == 0)
return i;
if (c < 0)
{
lo = i + 1;
}
else
{
hi = i - 1;
}
}
return ~lo;
}
}
}

2360
src/Avalonia.Visuals/Utilities/FrugalList.cs

File diff suppressed because it is too large

58
src/Avalonia.Visuals/Utilities/MappedArraySlice.cs

@ -0,0 +1,58 @@
// Copyright (c) Six Labors.
// Licensed under the Apache License, Version 2.0.
// Ported from: https://github.com/SixLabors/Fonts/
using System;
using System.Runtime.CompilerServices;
namespace Avalonia.Utilities
{
/// <summary>
/// Provides a mapped view of an underlying slice, selecting arbitrary indices
/// from the source array.
/// </summary>
/// <typeparam name="T">The type of item contained in the underlying array.</typeparam>
internal readonly struct MappedArraySlice<T>
where T : struct
{
private readonly ArraySlice<T> _data;
private readonly ArraySlice<int> _map;
/// <summary>
/// Initializes a new instance of the <see cref="MappedArraySlice{T}"/> struct.
/// </summary>
/// <param name="data">The data slice.</param>
/// <param name="map">The map slice.</param>
public MappedArraySlice(in ArraySlice<T> data, in ArraySlice<int> map)
{
#if DEBUG
if (map.Length.CompareTo(data.Length) > 0)
{
throw new ArgumentOutOfRangeException(nameof(map));
}
#endif
_data = data;
_map = map;
}
/// <summary>
/// Gets the number of items in the map.
/// </summary>
public int Length => _map.Length;
/// <summary>
/// Returns a reference to specified element of the slice.
/// </summary>
/// <param name="index">The index of the element to return.</param>
/// <returns>The <typeparamref name="T"/>.</returns>
/// <exception cref="IndexOutOfRangeException">
/// Thrown when index less than 0 or index greater than or equal to <see cref="Length"/>.
/// </exception>
public ref T this[int index]
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => ref _data[_map[index]];
}
}
}

80
src/Avalonia.Visuals/Utilities/ReadOnlySlice.cs

@ -2,6 +2,7 @@
using System.Collections; using System.Collections;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics; using System.Diagnostics;
using System.Runtime.CompilerServices;
namespace Avalonia.Utilities namespace Avalonia.Utilities
{ {
@ -10,15 +11,37 @@ namespace Avalonia.Utilities
/// </summary> /// </summary>
/// <typeparam name="T">The type of elements in the slice.</typeparam> /// <typeparam name="T">The type of elements in the slice.</typeparam>
[DebuggerTypeProxy(typeof(ReadOnlySlice<>.ReadOnlySliceDebugView))] [DebuggerTypeProxy(typeof(ReadOnlySlice<>.ReadOnlySliceDebugView))]
public readonly struct ReadOnlySlice<T> : IReadOnlyList<T> public readonly struct ReadOnlySlice<T> : IReadOnlyList<T> where T : struct
{ {
private readonly int _offset;
/// <summary>
/// Gets an empty <see cref="ReadOnlySlice{T}"/>
/// </summary>
public static ReadOnlySlice<T> Empty => new ReadOnlySlice<T>(Array.Empty<T>());
private readonly ReadOnlyMemory<T> _buffer;
public ReadOnlySlice(ReadOnlyMemory<T> buffer) : this(buffer, 0, buffer.Length) { } public ReadOnlySlice(ReadOnlyMemory<T> buffer) : this(buffer, 0, buffer.Length) { }
public ReadOnlySlice(ReadOnlyMemory<T> buffer, int start, int length) public ReadOnlySlice(ReadOnlyMemory<T> buffer, int start, int length, int offset = 0)
{ {
Buffer = buffer; #if DEBUG
if (start.CompareTo(0) < 0)
{
throw new ArgumentOutOfRangeException(nameof (start));
}
if (length.CompareTo(buffer.Length) > 0)
{
throw new ArgumentOutOfRangeException(nameof (length));
}
#endif
_buffer = buffer;
Start = start; Start = start;
Length = length; Length = length;
_offset = offset;
} }
/// <summary> /// <summary>
@ -51,12 +74,38 @@ namespace Avalonia.Utilities
public bool IsEmpty => Length == 0; public bool IsEmpty => Length == 0;
/// <summary> /// <summary>
/// The buffer. /// The underlying span.
/// </summary> /// </summary>
public ReadOnlyMemory<T> Buffer { get; } public ReadOnlySpan<T> Span => _buffer.Span.Slice(_offset, Length);
public T this[int index] => Buffer.Span[index]; /// <summary>
/// The underlying buffer.
/// </summary>
public ReadOnlyMemory<T> Buffer => _buffer;
/// <summary>
/// Returns a value to specified element of the slice.
/// </summary>
/// <param name="index">The index of the element to return.</param>
/// <returns>The <typeparamref name="T"/>.</returns>
/// <exception cref="IndexOutOfRangeException">
/// Thrown when index less than 0 or index greater than or equal to <see cref="Length"/>.
/// </exception>
public T 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> /// <summary>
/// Returns a sub slice of elements that start at the specified index and has the specified number of elements. /// Returns a sub slice of elements that start at the specified index and has the specified number of elements.
/// </summary> /// </summary>
@ -70,19 +119,22 @@ namespace Avalonia.Utilities
return this; return this;
} }
if (start < Start || start > End) if (length == 0)
{
return Empty;
}
if (start < 0 || _offset + start > _buffer.Length - 1)
{ {
throw new ArgumentOutOfRangeException(nameof(start)); throw new ArgumentOutOfRangeException(nameof(start));
} }
if (start + length > Start + Length) if (_offset + start + length > _buffer.Length)
{ {
throw new ArgumentOutOfRangeException(nameof(length)); throw new ArgumentOutOfRangeException(nameof(length));
} }
var bufferOffset = start - Start; return new ReadOnlySlice<T>(_buffer, start, length, _offset);
return new ReadOnlySlice<T>(Buffer.Slice(bufferOffset), start, length);
} }
/// <summary> /// <summary>
@ -102,7 +154,7 @@ namespace Avalonia.Utilities
throw new ArgumentOutOfRangeException(nameof(length)); throw new ArgumentOutOfRangeException(nameof(length));
} }
return new ReadOnlySlice<T>(Buffer.Slice(0, length), Start, length); return new ReadOnlySlice<T>(_buffer, Start, length, _offset);
} }
/// <summary> /// <summary>
@ -122,7 +174,7 @@ namespace Avalonia.Utilities
throw new ArgumentOutOfRangeException(nameof(length)); throw new ArgumentOutOfRangeException(nameof(length));
} }
return new ReadOnlySlice<T>(Buffer.Slice(length), Start + length, Length - length); return new ReadOnlySlice<T>(_buffer, Start + length, Length - length, _offset + length);
} }
/// <summary> /// <summary>
@ -174,7 +226,7 @@ namespace Avalonia.Utilities
public bool IsEmpty => _readOnlySlice.IsEmpty; public bool IsEmpty => _readOnlySlice.IsEmpty;
public ReadOnlyMemory<T> Items => _readOnlySlice.Buffer; public ReadOnlySpan<T> Items => _readOnlySlice.Span;
} }
} }
} }

596
src/Avalonia.Visuals/Utilities/Span.cs

@ -0,0 +1,596 @@
// 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.
//+-----------------------------------------------------------------------
//
//
//
// Contents: Generic span types
//
// [As of this creation, C# has no real generic type system]
//
using System;
using System.Collections;
using System.Diagnostics;
namespace Avalonia.Utilities
{
internal class Span
{
public readonly object? element;
public int length;
public Span(object? element, int length)
{
this.element = element;
this.length = length;
}
}
/// <summary>
/// VECTOR: A series of spans
/// </summary>
internal class SpanVector : IEnumerable
{
private static readonly Equals s_referenceEquals = object.ReferenceEquals;
private static readonly Equals s_equals = object.Equals;
private FrugalStructList<Span> _spans;
internal SpanVector(
object? defaultObject,
FrugalStructList<Span> spans = new FrugalStructList<Span>())
{
Default = defaultObject;
_spans = spans;
}
/// <summary>
/// Get enumerator to vector
/// </summary>
public IEnumerator GetEnumerator()
{
return new SpanEnumerator(this);
}
/// <summary>
/// Add a new span to vector
/// </summary>
private void Add(Span span)
{
_spans.Add(span);
}
/// <summary>
/// Delete n elements of vector
/// </summary>
internal virtual void Delete(int index, int count, ref SpanPosition latestPosition)
{
DeleteInternal(index, count);
if (index <= latestPosition.Index)
latestPosition = new SpanPosition();
}
private void DeleteInternal(int index, int count)
{
// Do removes highest index to lowest to minimize the number
// of array entries copied.
for (var i = index + count - 1; i >= index; --i)
{
_spans.RemoveAt(i);
}
}
/// <summary>
/// Insert n elements to vector
/// </summary>
private void Insert(int index, int count)
{
for (var c = 0; c < count; c++)
_spans.Insert(index, new Span(null, 0));
}
/// <summary>
/// Finds the span that contains the specified character position.
/// </summary>
/// <param name="cp">position to find</param>
/// <param name="latestPosition">Position of the most recently accessed span (e.g., the current span
/// of a SpanRider) for performance; FindSpan runs in O(1) time if the specified cp is in the same span
/// or an adjacent span.</param>
/// <param name="spanPosition">receives the index and first cp of the span that contains the specified
/// position or, if the position is past the end of the vector, the index and cp just past the end of
/// the last span.</param>
/// <returns>Returns true if cp is in range or false if not.</returns>
internal bool FindSpan(int cp, SpanPosition latestPosition, out SpanPosition spanPosition)
{
Debug.Assert(cp >= 0);
var spanCount = _spans.Count;
int spanIndex, spanCP;
if (cp == 0)
{
// CP zero always corresponds to span index zero
spanIndex = 0;
spanCP = 0;
}
else if (cp >= latestPosition.Offset || cp * 2 < latestPosition.Offset)
{
// One of the following is true:
// 1. cp is after the latest position (the most recently accessed span)
// 2. cp is closer to zero than to the latest position
if (cp >= latestPosition.Offset)
{
// case 1: scan forward from the latest position
spanIndex = latestPosition.Index;
spanCP = latestPosition.Offset;
}
else
{
// case 2: scan forward from the start of the span vector
spanIndex = 0;
spanCP = 0;
}
// Scan forward until we find the Span that contains the specified CP or
// reach the end of the SpanVector
for (; spanIndex < spanCount; ++spanIndex)
{
var spanLength = _spans[spanIndex].length;
if (cp < spanCP + spanLength)
{
break;
}
spanCP += spanLength;
}
}
else
{
// The specified CP is before the latest position but closer to it than to zero;
// therefore scan backwards from the latest position
spanIndex = latestPosition.Index;
spanCP = latestPosition.Offset;
while (spanCP > cp)
{
Debug.Assert(spanIndex > 0);
spanCP -= _spans[--spanIndex].length;
}
}
// Return index and cp of span in out param.
spanPosition = new SpanPosition(spanIndex, spanCP);
// Return true if the span is in range.
return spanIndex != spanCount;
}
/// <summary>
/// Set an element as a value to a character range
/// </summary>
/// <remarks>
/// Implementation of span element object must implement Object.Equals to
/// avoid runtime reflection cost on equality check of nested-type object.
/// </remarks>
public void SetValue(int first, int length, object element)
{
Set(first, length, element, SpanVector.s_equals, new SpanPosition());
}
/// <summary>
/// Set an element as a value to a character range; takes a SpanPosition of a recently accessed
/// span for performance and returns a known valid SpanPosition
/// </summary>
public SpanPosition SetValue(int first, int length, object element, SpanPosition spanPosition)
{
return Set(first, length, element, SpanVector.s_equals, spanPosition);
}
/// <summary>
/// Set an element as a reference to a character range
/// </summary>
public void SetReference(int first, int length, object element)
{
Set(first, length, element, SpanVector.s_referenceEquals, new SpanPosition());
}
/// <summary>
/// Set an element as a reference to a character range; takes a SpanPosition of a recently accessed
/// span for performance and returns a known valid SpanPosition
/// </summary>
public SpanPosition SetReference(int first, int length, object element, SpanPosition spanPosition)
{
return Set(first, length, element, SpanVector.s_referenceEquals, spanPosition);
}
private SpanPosition Set(int first, int length, object? element, Equals equals, SpanPosition spanPosition)
{
var inRange = FindSpan(first, spanPosition, out spanPosition);
// fs = index of first span partly or completely updated
// fc = character index at start of fs
var fs = spanPosition.Index;
var fc = spanPosition.Offset;
// Find the span that contains the first affected cp
if (!inRange)
{
// The first cp is past the end of the last span
if (fc < first)
{
// Create default run up to first
Add(new Span(Default, first - fc));
}
if (Count > 0
&& equals(_spans[Count - 1].element, element))
{
// New Element matches end Element, just extend end Element
_spans[Count - 1].length += length;
// Make sure fs and fc still agree
if (fs == Count)
{
fc += length;
}
}
else
{
Add(new Span(element, length));
}
}
else
{
// Now find the last span affected by the update
var ls = fs;
var lc = fc;
while (ls < Count
&& lc + _spans[ls].length <= first + length)
{
lc += _spans[ls].length;
ls++;
}
// ls = first span following update to remain unchanged in part or in whole
// lc = character index at start of ls
// expand update region backwards to include existing Spans of identical
// Element type
if (first == fc)
{
// Item at [fs] is completely replaced. Check prior item
if (fs > 0
&& equals(_spans[fs - 1].element, element))
{
// Expand update area over previous run of equal classification
fs--;
fc -= _spans[fs].length;
first = fc;
length += _spans[fs].length;
}
}
else
{
// Item at [fs] is partially replaced. Check if it is same as update
if (equals(_spans[fs].element, element))
{
// Expand update area back to start of first affected equal valued run
length = first + length - fc;
first = fc;
}
}
// Expand update region forwards to include existing Spans of identical
// Element type
if (ls < Count
&& equals(_spans[ls].element, element))
{
// Extend update region to end of existing split run
length = lc + _spans[ls].length - first;
lc += _spans[ls].length;
ls++;
}
// If no old Spans remain beyond area affected by update, handle easily:
if (ls >= Count)
{
// None of the old span list extended beyond the update region
if (fc < first)
{
// Updated region leaves some of [fs]
if (Count != fs + 2)
{
if (!Resize(fs + 2))
throw new OutOfMemoryException();
}
_spans[fs].length = first - fc;
_spans[fs + 1] = new Span(element, length);
}
else
{
// Updated item replaces [fs]
if (Count != fs + 1)
{
if (!Resize(fs + 1))
throw new OutOfMemoryException();
}
_spans[fs] = new Span(element, length);
}
}
else
{
// Record partial element type at end, if any
object? trailingElement = null;
var trailingLength = 0;
if (first + length > lc)
{
trailingElement = _spans[ls].element;
trailingLength = lc + _spans[ls].length - (first + length);
}
// Calculate change in number of Spans
var spanDelta = 1 // The new span
+ (first > fc ? 1 : 0) // part span at start
- (ls - fs); // existing affected span count
// Note part span at end doesn't affect the calculation - the run may need
// updating, but it doesn't need creating.
if (spanDelta < 0)
{
DeleteInternal(fs + 1, -spanDelta);
}
else if (spanDelta > 0)
{
Insert(fs + 1, spanDelta);
// Initialize inserted Spans
for (var i = 0; i < spanDelta; i++)
{
_spans[fs + 1 + i] = new Span(null, 0);
}
}
// Assign Element values
// Correct Length of split span before updated range
if (fc < first)
{
_spans[fs].length = first - fc;
fs++;
fc = first;
}
// Record Element type for updated range
_spans[fs] = new Span(element, length);
fs++;
fc += length;
// Correct Length of split span following updated range
if (lc < first + length)
{
_spans[fs] = new Span(trailingElement, trailingLength);
}
}
}
// Return a known valid span position.
return new SpanPosition(fs, fc);
}
/// <summary>
/// Number of spans in vector
/// </summary>
public int Count
{
get { return _spans.Count; }
}
/// <summary>
/// The default element of vector
/// </summary>
public object? Default { get; }
/// <summary>
/// Span accessor at nth element
/// </summary>
public Span this[int index]
{
get { return _spans[index]; }
}
private bool Resize(int targetCount)
{
if (targetCount > Count)
{
for (var c = 0; c < targetCount - Count; c++)
{
_spans.Add(new Span(null, 0));
}
}
else if (targetCount < Count)
{
DeleteInternal(targetCount, Count - targetCount);
}
return true;
}
}
/// <summary>
/// Equality check method
/// </summary>
internal delegate bool Equals(object? first, object? second);
/// <summary>
/// ENUMERATOR: To navigate a vector through its element
/// </summary>
internal sealed class SpanEnumerator : IEnumerator
{
private readonly SpanVector _spans;
private int _current; // current span
internal SpanEnumerator(SpanVector spans)
{
_spans = spans;
_current = -1;
}
/// <summary>
/// The current span
/// </summary>
public object Current
{
get { return _spans[_current]; }
}
/// <summary>
/// Move to the next span
/// </summary>
public bool MoveNext()
{
_current++;
return _current < _spans.Count;
}
/// <summary>
/// Reset the enumerator
/// </summary>
public void Reset()
{
_current = -1;
}
}
/// <summary>
/// Represents a Span's position as a pair of related values: its index in the
/// SpanVector its CP offset from the start of the SpanVector.
/// </summary>
internal readonly struct SpanPosition
{
internal SpanPosition(int spanIndex, int spanOffset)
{
Index = spanIndex;
Offset = spanOffset;
}
internal int Index { get; }
internal int Offset { get; }
}
/// <summary>
/// RIDER: To navigate a vector through character index
/// </summary>
internal struct SpanRider
{
private readonly SpanVector _spans; // vector of spans
private SpanPosition _spanPosition; // index and cp of current span
public SpanRider(SpanVector spans, SpanPosition latestPosition) : this(spans, latestPosition, latestPosition.Offset)
{
}
public SpanRider(SpanVector spans, SpanPosition latestPosition = new SpanPosition(), int cp = 0)
{
_spans = spans;
_spanPosition = new SpanPosition();
CurrentPosition = 0;
Length = 0;
At(latestPosition, cp);
}
/// <summary>
/// Move rider to a given cp
/// </summary>
public bool At(int cp)
{
return At(_spanPosition, cp);
}
public bool At(SpanPosition latestPosition, int cp)
{
var inRange = _spans.FindSpan(cp, latestPosition, out _spanPosition);
if (inRange)
{
// cp is in range:
// - Length is the distance to the end of the span
// - CurrentPosition is cp
Length = _spans[_spanPosition.Index].length - (cp - _spanPosition.Offset);
CurrentPosition = cp;
}
else
{
// cp is out of range:
// - Length is the default span length
// - CurrentPosition is the end of the last span
Length = int.MaxValue;
CurrentPosition = _spanPosition.Offset;
}
return inRange;
}
/// <summary>
/// The first cp of the current span
/// </summary>
public int CurrentSpanStart
{
get { return _spanPosition.Offset; }
}
/// <summary>
/// The length of current span start from the current cp
/// </summary>
public int Length { get; private set; }
/// <summary>
/// The current position
/// </summary>
public int CurrentPosition { get; private set; }
/// <summary>
/// The element of the current span
/// </summary>
public object? CurrentElement
{
get { return _spanPosition.Index >= _spans.Count ? _spans.Default : _spans[_spanPosition.Index].element; }
}
/// <summary>
/// Index of the span at the current position.
/// </summary>
public int CurrentSpanIndex
{
get { return _spanPosition.Index; }
}
/// <summary>
/// Index and first cp of the current span.
/// </summary>
public SpanPosition SpanPosition
{
get { return _spanPosition; }
}
}
}

17
src/Shared/PlatformSupport/PlatformSupport.projitems

@ -1,17 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<MSBuildAllProjects>$(MSBuildAllProjects);$(MSBuildThisFileFullPath)</MSBuildAllProjects>
<HasSharedItems>true</HasSharedItems>
<SharedGUID>{E4D9629C-F168-4224-8F51-F5E482FFEC42}</SharedGUID>
</PropertyGroup>
<PropertyGroup Label="Configuration">
<Import_RootNamespace>Avalonia.Shared.PlatformSupport</Import_RootNamespace>
</PropertyGroup>
<ItemGroup>
<Compile Include="$(MSBuildThisFileDirectory)AssetLoader.cs" />
<Compile Include="$(MSBuildThisFileDirectory)DynLoader.cs" />
<Compile Include="$(MSBuildThisFileDirectory)StandardRuntimePlatform.cs" />
<Compile Include="$(MSBuildThisFileDirectory)StandardRuntimePlatformServices.cs" />
</ItemGroup>
</Project>

11
src/Shared/PlatformSupport/PlatformSupport.shproj

@ -1,11 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<ProjectGuid>{E4D9629C-F168-4224-3F51-A5E482FFBC42}</ProjectGuid>
</PropertyGroup>
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
<Import Project="$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)\CodeSharing\Microsoft.CodeSharing.Common.Default.props" />
<Import Project="$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)\CodeSharing\Microsoft.CodeSharing.Common.props" />
<Import Project="PlatformSupport.projitems" Label="Shared" />
<Import Project="$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)\CodeSharing\Microsoft.CodeSharing.CSharp.targets" />
</Project>

28
src/Shared/PlatformSupport/StandardRuntimePlatformServices.cs

@ -1,28 +0,0 @@
using System.Reflection;
using System.Runtime.InteropServices;
using Avalonia.Platform;
using Avalonia.Platform.Interop;
namespace Avalonia.Shared.PlatformSupport
{
static class StandardRuntimePlatformServices
{
public static void Register(Assembly assembly = null)
{
var standardPlatform = new StandardRuntimePlatform();
AssetLoader.RegisterResUriParsers();
AvaloniaLocator.CurrentMutable
.Bind<IRuntimePlatform>().ToConstant(standardPlatform)
.Bind<IAssetLoader>().ToConstant(new AssetLoader(assembly))
.Bind<IDynamicLibraryLoader>().ToConstant(
#if __IOS__
new IOSLoader()
#else
RuntimeInformation.IsOSPlatform(OSPlatform.Windows)
? (IDynamicLibraryLoader)new Win32Loader()
: new UnixLoader()
#endif
);
}
}
}

13
src/Shared/RenderHelpers/QuadBezierHelper.cs

@ -1,13 +0,0 @@
using Avalonia.Platform;
namespace Avalonia.RenderHelpers
{
static class QuadBezierHelper
{
public static void QuadraticBezierTo(IStreamGeometryContextImpl context, Point current, Point controlPoint, Point endPoint)
{
//(s, (s + 2c)/ 3, (e + 2c)/ 3, e)
context.CubicBezierTo((current + 2*controlPoint)/3, (endPoint + 2*controlPoint)/3, endPoint);
}
}
}

14
src/Shared/RenderHelpers/RenderHelpers.projitems

@ -1,14 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<MSBuildAllProjects>$(MSBuildAllProjects);$(MSBuildThisFileFullPath)</MSBuildAllProjects>
<HasSharedItems>true</HasSharedItems>
<SharedGUID>3c4c0cb4-0c0f-4450-a37b-148c84ff905f</SharedGUID>
</PropertyGroup>
<PropertyGroup Label="Configuration">
<Import_RootNamespace>Avalonia.RenderHelpers</Import_RootNamespace>
</PropertyGroup>
<ItemGroup>
<Compile Include="$(MSBuildThisFileDirectory)QuadBezierHelper.cs" />
</ItemGroup>
</Project>

13
src/Shared/RenderHelpers/RenderHelpers.shproj

@ -1,13 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="14.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup Label="Globals">
<ProjectGuid>3c4c0cb4-0c0f-4450-a37b-148c84ff905f</ProjectGuid>
<MinimumVisualStudioVersion>14.0</MinimumVisualStudioVersion>
</PropertyGroup>
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
<Import Project="$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)\CodeSharing\Microsoft.CodeSharing.Common.Default.props" />
<Import Project="$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)\CodeSharing\Microsoft.CodeSharing.Common.props" />
<PropertyGroup />
<Import Project="RenderHelpers.projitems" Label="Shared" />
<Import Project="$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)\CodeSharing\Microsoft.CodeSharing.CSharp.targets" />
</Project>

84
src/Shared/WindowResizeDragHelper.cs

@ -1,84 +0,0 @@
using System;
using Avalonia.Controls;
using Avalonia.Input.Raw;
using Avalonia.Platform;
namespace Avalonia
{
internal class ManagedWindowResizeDragHelper
{
private readonly IWindowBaseImpl _window;
private readonly Action<bool> _captureMouse;
private readonly Action<Rect> _resize;
private WindowEdge? _edge;
private Point _prevPoint;
public ManagedWindowResizeDragHelper(IWindowBaseImpl window, Action<bool> captureMouse, Action<Rect> resize = null)
{
_window = window;
_captureMouse = captureMouse;
_resize = resize;
}
public void BeginResizeDrag(WindowEdge edge, Point currentMousePosition)
{
_captureMouse(true);
_prevPoint = currentMousePosition;
_edge = edge;
}
public bool PreprocessInputEvent(ref RawInputEventArgs e)
{
if (_edge == null)
return false;
if (e is RawMouseEventArgs args)
{
if (args.Type == RawMouseEventType.LeftButtonUp)
{
_edge = null;
_captureMouse(false);
}
if (args.Type == RawMouseEventType.Move)
{
MoveWindow(args.Position);
return true;
}
_edge = null;
}
return false;
}
private void MoveWindow(Point position)
{
var diff = position - _prevPoint;
var edge = _edge.Value;
var rc = new Rect(_window.Position, _window.ClientSize);
if (edge == WindowEdge.East || edge == WindowEdge.NorthEast || edge == WindowEdge.SouthEast)
{
rc = rc.WithWidth(rc.Width + diff.X);
_prevPoint = _prevPoint.WithX(position.X);
}
if (edge == WindowEdge.West || edge == WindowEdge.NorthWest || edge == WindowEdge.SouthWest)
rc = rc.WithX(rc.X + diff.X).WithWidth(rc.Width - diff.X);
if (edge == WindowEdge.South || edge == WindowEdge.SouthWest || edge == WindowEdge.SouthEast)
{
rc = rc.WithHeight(rc.Height + diff.Y);
_prevPoint = _prevPoint.WithY(position.Y);
}
if (edge == WindowEdge.North || edge == WindowEdge.NorthWest || edge == WindowEdge.NorthEast)
rc = rc.WithY(rc.Y + diff.Y).WithHeight(rc.Height - diff.Y);
if (_resize != null)
_resize(rc);
else
{
if (_window.Position != rc.Position)
_window.Position = rc.Position;
if (_window.ClientSize != rc.Size)
_window.Resize(rc.Size);
}
}
}
}

3
src/Skia/Avalonia.Skia/Avalonia.Skia.csproj

@ -16,6 +16,5 @@
</ItemGroup> </ItemGroup>
<Import Project="..\..\..\build\SkiaSharp.props" /> <Import Project="..\..\..\build\SkiaSharp.props" />
<Import Project="..\..\..\build\HarfBuzzSharp.props" /> <Import Project="..\..\..\build\HarfBuzzSharp.props" />
<Import Project="..\..\Shared\RenderHelpers\RenderHelpers.projitems" Label="Shared" />
</Project> </Project>

12
src/Skia/Avalonia.Skia/DrawingContextImpl.cs

@ -441,17 +441,7 @@ namespace Avalonia.Skia
} }
} }
} }
/// <inheritdoc />
public void DrawText(IBrush foreground, Point origin, IFormattedTextImpl text)
{
using (var paint = CreatePaint(_fillPaint, foreground, text.Bounds.Size))
{
var textImpl = (FormattedTextImpl) text;
textImpl.Draw(this, Canvas, origin.ToSKPoint(), paint, _canTextUseLcdRendering);
}
}
/// <inheritdoc /> /// <inheritdoc />
public void DrawGlyphRun(IBrush foreground, GlyphRun glyphRun) public void DrawGlyphRun(IBrush foreground, GlyphRun glyphRun)
{ {

838
src/Skia/Avalonia.Skia/FormattedTextImpl.cs

@ -1,838 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using Avalonia.Media;
using Avalonia.Platform;
using Avalonia.Utilities;
using SkiaSharp;
namespace Avalonia.Skia
{
/// <summary>
/// Skia formatted text implementation.
/// </summary>
internal class FormattedTextImpl : IFormattedTextImpl
{
private static readonly ThreadLocal<SKTextBlobBuilder> t_builder = new ThreadLocal<SKTextBlobBuilder>(() => new SKTextBlobBuilder());
private const float MAX_LINE_WIDTH = 10000;
private readonly List<KeyValuePair<FBrushRange, IBrush>> _foregroundBrushes =
new List<KeyValuePair<FBrushRange, IBrush>>();
private readonly List<FormattedTextLine> _lines = new List<FormattedTextLine>();
private readonly SKPaint _paint;
private readonly List<Rect> _rects = new List<Rect>();
public string Text { get; }
private readonly TextWrapping _wrapping;
private Size _constraint = new Size(double.PositiveInfinity, double.PositiveInfinity);
private float _lineHeight = 0;
private float _lineOffset = 0;
private Rect _bounds;
private List<AvaloniaFormattedTextLine> _skiaLines;
private ReadOnlySlice<ushort> _glyphs;
private ReadOnlySlice<float> _advances;
public FormattedTextImpl(
string text,
Typeface typeface,
double fontSize,
TextAlignment textAlignment,
TextWrapping wrapping,
Size constraint,
IReadOnlyList<FormattedTextStyleSpan> spans)
{
Text = text ?? string.Empty;
UpdateGlyphInfo(Text, typeface.GlyphTypeface, (float)fontSize);
_paint = new SKPaint
{
TextEncoding = SKTextEncoding.Utf16,
IsStroke = false,
IsAntialias = true,
LcdRenderText = true,
SubpixelText = true,
IsLinearText = true,
Typeface = ((GlyphTypefaceImpl)typeface.GlyphTypeface.PlatformImpl).Typeface,
TextSize = (float)fontSize,
TextAlign = textAlignment.ToSKTextAlign()
};
//currently Skia does not measure properly with Utf8 !!!
//Paint.TextEncoding = SKTextEncoding.Utf8;
_wrapping = wrapping;
_constraint = constraint;
if (spans != null)
{
foreach (var span in spans)
{
if (span.ForegroundBrush != null)
{
SetForegroundBrush(span.ForegroundBrush, span.StartIndex, span.Length);
}
}
}
Rebuild();
}
public Size Constraint => _constraint;
public Rect Bounds => _bounds;
public IEnumerable<FormattedTextLine> GetLines()
{
return _lines;
}
public TextHitTestResult HitTestPoint(Point point)
{
float y = (float)point.Y;
AvaloniaFormattedTextLine line = default;
float nextTop = 0;
foreach(var currentLine in _skiaLines)
{
if(currentLine.Top <= y)
{
line = currentLine;
nextTop = currentLine.Top + currentLine.Height;
}
else
{
nextTop = currentLine.Top;
break;
}
}
if (!line.Equals(default(AvaloniaFormattedTextLine)))
{
var rects = GetRects();
for (int c = line.Start; c < line.Start + line.TextLength; c++)
{
var rc = rects[c];
if (rc.Contains(point))
{
return new TextHitTestResult
{
IsInside = !(line.TextLength > line.Length),
TextPosition = c,
IsTrailing = (point.X - rc.X) > rc.Width / 2
};
}
}
int offset = 0;
if (point.X >= (rects[line.Start].X + line.Width) && line.Length > 0)
{
offset = line.TextLength > line.Length ?
line.Length : (line.Length - 1);
}
if (y < nextTop)
{
return new TextHitTestResult
{
IsInside = false,
TextPosition = line.Start + offset,
IsTrailing = Text.Length == (line.Start + offset + 1)
};
}
}
bool end = point.X > _bounds.Width || point.Y > _lines.Sum(l => l.Height);
return new TextHitTestResult()
{
IsInside = false,
IsTrailing = end,
TextPosition = end ? Text.Length - 1 : 0
};
}
public Rect HitTestTextPosition(int index)
{
if (string.IsNullOrEmpty(Text))
{
var alignmentOffset = TransformX(0, 0, _paint.TextAlign);
return new Rect(alignmentOffset, 0, 0, _lineHeight);
}
var rects = GetRects();
if (index >= Text.Length || index < 0)
{
var r = rects.LastOrDefault();
var c = Text[Text.Length - 1];
switch (c)
{
case '\n':
case '\r':
return new Rect(r.X, r.Y, 0, _lineHeight);
default:
return new Rect(r.X + r.Width, r.Y, 0, _lineHeight);
}
}
return rects[index];
}
public IEnumerable<Rect> HitTestTextRange(int index, int length)
{
List<Rect> result = new List<Rect>();
var rects = GetRects();
int lastIndex = index + length - 1;
foreach (var line in _skiaLines.Where(l =>
(l.Start + l.Length) > index &&
lastIndex >= l.Start &&
!l.IsEmptyTrailingLine))
{
int lineEndIndex = line.Start + (line.Length > 0 ? line.Length - 1 : 0);
double left = rects[line.Start > index ? line.Start : index].X;
double right = rects[lineEndIndex > lastIndex ? lastIndex : lineEndIndex].Right;
result.Add(new Rect(left, line.Top, right - left, line.Height));
}
return result;
}
public override string ToString()
{
return Text;
}
private void DrawTextBlob(int start, int length, float x, float y, SKCanvas canvas, SKPaint paint)
{
if(length == 0)
{
return;
}
var glyphs = _glyphs.Buffer.Span.Slice(start, length);
var advances = _advances.Buffer.Span.Slice(start, length);
var builder = t_builder.Value;
var buffer = builder.AllocateHorizontalRun(_paint.ToFont(), length, 0);
buffer.SetGlyphs(glyphs);
var positions = buffer.GetPositionSpan();
var pos = 0f;
for (int i = 0; i < advances.Length; i++)
{
positions[i] = pos;
pos += advances[i];
}
var blob = builder.Build();
if(blob != null)
{
canvas.DrawText(blob, x, y, paint);
}
}
internal void Draw(DrawingContextImpl context,
SKCanvas canvas,
SKPoint origin,
DrawingContextImpl.PaintWrapper foreground,
bool canUseLcdRendering)
{
/* TODO: This originated from Native code, it might be useful for debugging character positions as
* we improve the FormattedText support. Will need to port this to C# obviously. Rmove when
* not needed anymore.
SkPaint dpaint;
ctx->Canvas->save();
ctx->Canvas->translate(origin.fX, origin.fY);
for (int c = 0; c < Lines.size(); c++)
{
dpaint.setARGB(255, 0, 0, 0);
SkRect rc;
rc.fLeft = 0;
rc.fTop = Lines[c].Top;
rc.fRight = Lines[c].Width;
rc.fBottom = rc.fTop + LineOffset;
ctx->Canvas->drawRect(rc, dpaint);
}
for (int c = 0; c < Length; c++)
{
dpaint.setARGB(255, c % 10 * 125 / 10 + 125, (c * 7) % 10 * 250 / 10, (c * 13) % 10 * 250 / 10);
dpaint.setStyle(SkPaint::kFill_Style);
ctx->Canvas->drawRect(Rects[c], dpaint);
}
ctx->Canvas->restore();
*/
using (var paint = _paint.Clone())
{
IDisposable currd = null;
var currentWrapper = foreground;
SKPaint currentPaint = null;
try
{
ApplyWrapperTo(ref currentPaint, foreground, ref currd, paint, canUseLcdRendering);
bool hasCusomFGBrushes = _foregroundBrushes.Any();
for (int c = 0; c < _skiaLines.Count; c++)
{
AvaloniaFormattedTextLine line = _skiaLines[c];
float x = TransformX(origin.X, line.Width, paint.TextAlign);
if (!hasCusomFGBrushes)
{
DrawTextBlob(line.Start, line.Length, x, origin.Y + line.Top + _lineOffset, canvas, paint);
}
else
{
float currX = x;
float measure;
int len;
float factor;
switch (paint.TextAlign)
{
case SKTextAlign.Left:
factor = 0;
break;
case SKTextAlign.Center:
factor = 0.5f;
break;
case SKTextAlign.Right:
factor = 1;
break;
default:
throw new ArgumentOutOfRangeException();
}
currX -= line.Length == 0 ? 0 : MeasureText(line.Start, line.Length) * factor;
for (int i = line.Start; i < line.Start + line.Length;)
{
var fb = GetNextForegroundBrush(ref line, i, out len);
if (fb != null)
{
//TODO: figure out how to get the brush size
currentWrapper = context.CreatePaint(new SKPaint { IsAntialias = true }, fb,
new Size());
}
else
{
if (!currentWrapper.Equals(foreground)) currentWrapper.Dispose();
currentWrapper = foreground;
}
measure = MeasureText(i, len);
currX += measure * factor;
ApplyWrapperTo(ref currentPaint, currentWrapper, ref currd, paint, canUseLcdRendering);
DrawTextBlob(i, len, currX, origin.Y + line.Top + _lineOffset, canvas, paint);
i += len;
currX += measure * (1 - factor);
}
}
}
}
finally
{
if (!currentWrapper.Equals(foreground)) currentWrapper.Dispose();
currd?.Dispose();
}
}
}
private static void ApplyWrapperTo(ref SKPaint current, DrawingContextImpl.PaintWrapper wrapper,
ref IDisposable curr, SKPaint paint, bool canUseLcdRendering)
{
if (current == wrapper.Paint)
return;
curr?.Dispose();
curr = wrapper.ApplyTo(paint);
paint.LcdRenderText = canUseLcdRendering;
}
private static bool IsBreakChar(char c)
{
//white space or zero space whitespace
return char.IsWhiteSpace(c) || c == '\u200B';
}
private static int LineBreak(string textInput, int textIndex, int stop,
SKPaint paint, float maxWidth,
out int trailingCount)
{
int lengthBreak;
if (maxWidth == -1)
{
lengthBreak = stop - textIndex;
}
else
{
string subText = textInput.Substring(textIndex, stop - textIndex);
lengthBreak = (int)paint.BreakText(subText, maxWidth, out _);
}
//Check for white space or line breakers before the lengthBreak
int startIndex = textIndex;
int index = textIndex;
int word_start = textIndex;
bool prevBreak = true;
trailingCount = 0;
while (index < stop)
{
int prevText = index;
char currChar = textInput[index++];
bool currBreak = IsBreakChar(currChar);
if (!currBreak && prevBreak)
{
word_start = prevText;
}
prevBreak = currBreak;
if (index > startIndex + lengthBreak)
{
if (currBreak)
{
// eat the rest of the whitespace
while (index < stop && IsBreakChar(textInput[index]))
{
index++;
}
trailingCount = index - prevText;
}
else
{
// backup until a whitespace (or 1 char)
if (word_start == startIndex)
{
if (prevText > startIndex)
{
index = prevText;
}
}
else
{
index = word_start;
}
}
break;
}
if ('\n' == currChar)
{
int ret = index - startIndex;
int lineBreakSize = 1;
if (index < stop)
{
currChar = textInput[index++];
if ('\r' == currChar)
{
ret = index - startIndex;
++lineBreakSize;
}
}
trailingCount = lineBreakSize;
return ret;
}
if ('\r' == currChar)
{
int ret = index - startIndex;
int lineBreakSize = 1;
if (index < stop)
{
currChar = textInput[index++];
if ('\n' == currChar)
{
ret = index - startIndex;
++lineBreakSize;
}
}
trailingCount = lineBreakSize;
return ret;
}
}
return index - startIndex;
}
private void BuildRects()
{
// Build character rects
SKTextAlign align = _paint.TextAlign;
for (int li = 0; li < _skiaLines.Count; li++)
{
var line = _skiaLines[li];
float prevRight = TransformX(0, line.Width, align);
double nextTop = line.Top + line.Height;
if (li + 1 < _skiaLines.Count)
{
nextTop = _skiaLines[li + 1].Top;
}
for (int i = line.Start; i < line.Start + line.TextLength; i++)
{
var w = line.IsEmptyTrailingLine ? 0 : _advances[i];
_rects.Add(new Rect(
prevRight,
line.Top,
w,
nextTop - line.Top));
prevRight += w;
}
}
}
private IBrush GetNextForegroundBrush(ref AvaloniaFormattedTextLine line, int index, out int length)
{
IBrush result = null;
int len = length = line.Start + line.Length - index;
if (_foregroundBrushes.Any())
{
var bi = _foregroundBrushes.FindIndex(b =>
b.Key.StartIndex <= index &&
b.Key.EndIndex > index
);
if (bi > -1)
{
var match = _foregroundBrushes[bi];
len = match.Key.EndIndex - index;
result = match.Value;
if (len > 0 && len < length)
{
length = len;
}
}
int endIndex = index + length;
int max = bi == -1 ? _foregroundBrushes.Count : bi;
var next = _foregroundBrushes.Take(max)
.Where(b => b.Key.StartIndex < endIndex &&
b.Key.StartIndex > index)
.OrderBy(b => b.Key.StartIndex)
.FirstOrDefault();
if (next.Value != null)
{
length = next.Key.StartIndex - index;
}
}
return result;
}
private List<Rect> GetRects()
{
if (Text.Length > _rects.Count)
{
BuildRects();
}
return _rects;
}
private void Rebuild()
{
var length = Text.Length;
_lines.Clear();
_rects.Clear();
_skiaLines = new List<AvaloniaFormattedTextLine>();
int curOff = 0;
float curY = 0;
var metrics = _paint.FontMetrics;
var mTop = metrics.Top; // The greatest distance above the baseline for any glyph (will be <= 0).
var mBottom = metrics.Bottom; // The greatest distance below the baseline for any glyph (will be >= 0).
var mLeading = metrics.Leading; // The recommended distance to add between lines of text (will be >= 0).
var mDescent = metrics.Descent; //The recommended distance below the baseline. Will be >= 0.
var mAscent = metrics.Ascent; //The recommended distance above the baseline. Will be <= 0.
var lastLineDescent = mBottom - mDescent;
// This seems like the best measure of full vertical extent
// matches Direct2D line height
_lineHeight = mDescent - mAscent + metrics.Leading;
// Rendering is relative to baseline
_lineOffset = (-metrics.Ascent);
string subString;
float widthConstraint = double.IsPositiveInfinity(_constraint.Width)
? -1
: (float)_constraint.Width;
while(curOff < length)
{
float lineWidth = -1;
int measured;
int trailingnumber = 0;
float constraint = -1;
if (_wrapping == TextWrapping.Wrap)
{
constraint = widthConstraint <= 0 ? MAX_LINE_WIDTH : widthConstraint;
if (constraint > MAX_LINE_WIDTH)
constraint = MAX_LINE_WIDTH;
}
measured = LineBreak(Text, curOff, length, _paint, constraint, out trailingnumber);
AvaloniaFormattedTextLine line = new AvaloniaFormattedTextLine();
line.Start = curOff;
line.TextLength = measured;
subString = Text.Substring(line.Start, line.TextLength);
lineWidth = MeasureText(line.Start, line.TextLength);
line.Length = measured - trailingnumber;
line.Width = lineWidth;
line.Height = _lineHeight;
line.Top = curY;
_skiaLines.Add(line);
curY += _lineHeight;
curY += mLeading;
curOff += measured;
//if this is the last line and there are trailing newline characters then
//insert a additional line
if (curOff >= length)
{
var subStringMinusNewlines = subString.TrimEnd('\n', '\r');
var lengthDiff = subString.Length - subStringMinusNewlines.Length;
if (lengthDiff > 0)
{
AvaloniaFormattedTextLine lastLine = new AvaloniaFormattedTextLine();
lastLine.TextLength = lengthDiff;
lastLine.Start = curOff - lengthDiff;
var lastLineWidth = MeasureText(line.Start, line.TextLength);
lastLine.Length = 0;
lastLine.Width = lastLineWidth;
lastLine.Height = _lineHeight;
lastLine.Top = curY;
lastLine.IsEmptyTrailingLine = true;
_skiaLines.Add(lastLine);
curY += _lineHeight;
curY += mLeading;
}
}
}
// Now convert to Avalonia data formats
_lines.Clear();
float maxX = 0;
for (var c = 0; c < _skiaLines.Count; c++)
{
var w = _skiaLines[c].Width;
if (maxX < w)
maxX = w;
_lines.Add(new FormattedTextLine(_skiaLines[c].TextLength, _skiaLines[c].Height));
}
if (_skiaLines.Count == 0)
{
_lines.Add(new FormattedTextLine(0, _lineHeight));
_bounds = new Rect(0, 0, 0, _lineHeight);
}
else
{
var lastLine = _skiaLines[_skiaLines.Count - 1];
_bounds = new Rect(0, 0, maxX, lastLine.Top + lastLine.Height);
if (double.IsPositiveInfinity(Constraint.Width))
{
return;
}
switch (_paint.TextAlign)
{
case SKTextAlign.Center:
_bounds = new Rect(Constraint).CenterRect(_bounds);
break;
case SKTextAlign.Right:
_bounds = new Rect(
Constraint.Width - _bounds.Width,
0,
_bounds.Width,
_bounds.Height);
break;
}
}
}
private float MeasureText(int start, int length)
{
var width = 0f;
for (int i = start; i < start + length; i++)
{
var advance = _advances[i];
width += advance;
}
return width;
}
private void UpdateGlyphInfo(string text, GlyphTypeface glyphTypeface, float fontSize)
{
var glyphs = new ushort[text.Length];
var advances = new float[text.Length];
var scale = fontSize / glyphTypeface.DesignEmHeight;
var width = 0f;
var characters = text.AsSpan();
for (int i = 0; i < characters.Length; i++)
{
var c = characters[i];
float advance;
ushort glyph;
switch (c)
{
case (char)0:
{
glyph = glyphTypeface.GetGlyph(0x200B);
advance = 0;
break;
}
case '\t':
{
glyph = glyphTypeface.GetGlyph(' ');
advance = glyphTypeface.GetGlyphAdvance(glyph) * scale * 4;
break;
}
default:
{
glyph = glyphTypeface.GetGlyph(c);
advance = glyphTypeface.GetGlyphAdvance(glyph) * scale;
break;
}
}
glyphs[i] = glyph;
advances[i] = advance;
width += advance;
}
_glyphs = new ReadOnlySlice<ushort>(glyphs);
_advances = new ReadOnlySlice<float>(advances);
}
private float TransformX(float originX, float lineWidth, SKTextAlign align)
{
float x = 0;
if (align == SKTextAlign.Left)
{
x = originX;
}
else
{
double width = Constraint.Width > 0 && !double.IsPositiveInfinity(Constraint.Width) ?
Constraint.Width :
_bounds.Width;
switch (align)
{
case SKTextAlign.Center: x = originX + (float)(width - lineWidth) / 2; break;
case SKTextAlign.Right: x = originX + (float)(width - lineWidth); break;
}
}
return x;
}
private void SetForegroundBrush(IBrush brush, int startIndex, int length)
{
var key = new FBrushRange(startIndex, length);
int index = _foregroundBrushes.FindIndex(v => v.Key.Equals(key));
if (index > -1)
{
_foregroundBrushes.RemoveAt(index);
}
if (brush != null)
{
brush = brush.ToImmutable();
_foregroundBrushes.Insert(0, new KeyValuePair<FBrushRange, IBrush>(key, brush));
}
}
private struct AvaloniaFormattedTextLine
{
public float Height;
public int Length;
public int Start;
public int TextLength;
public float Top;
public float Width;
public bool IsEmptyTrailingLine;
};
private struct FBrushRange
{
public FBrushRange(int startIndex, int length)
{
StartIndex = startIndex;
Length = length;
}
public int EndIndex => StartIndex + Length;
public int Length { get; private set; }
public int StartIndex { get; private set; }
public bool Intersects(int index, int len) =>
(index + len) > StartIndex &&
(StartIndex + Length) > index;
public override string ToString()
{
return $"{StartIndex}-{EndIndex}";
}
}
}
}

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

Loading…
Cancel
Save