Browse Source

Merge branch 'master' into fix/textBoxColors

pull/9631/head
Benedikt Stebner 4 years ago
committed by GitHub
parent
commit
2f96acec65
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 6
      .editorconfig
  2. 5
      .ncrunch/ReactiveUIDemo.v3.ncrunchproject
  3. 1
      Avalonia.Desktop.slnf
  4. 18
      Avalonia.sln
  5. 3
      samples/ControlCatalog/MainView.xaml
  6. 2
      samples/ControlCatalog/Pages/ImagePage.xaml.cs
  7. 27
      samples/ControlCatalog/Pages/RefreshContainerPage.axaml
  8. 36
      samples/ControlCatalog/Pages/RefreshContainerPage.axaml.cs
  9. 26
      samples/ControlCatalog/ViewModels/RefreshContainerViewModel.cs
  10. 8
      samples/ReactiveUIDemo/App.axaml
  11. 37
      samples/ReactiveUIDemo/App.axaml.cs
  12. 19
      samples/ReactiveUIDemo/MainWindow.axaml
  13. 22
      samples/ReactiveUIDemo/MainWindow.axaml.cs
  14. 28
      samples/ReactiveUIDemo/ReactiveUIDemo.csproj
  15. 11
      samples/ReactiveUIDemo/ViewModels/BarViewModel.cs
  16. 11
      samples/ReactiveUIDemo/ViewModels/FooViewModel.cs
  17. 9
      samples/ReactiveUIDemo/ViewModels/MainWindowViewModel.cs
  18. 21
      samples/ReactiveUIDemo/ViewModels/RoutedViewHostPageViewModel.cs
  19. 16
      samples/ReactiveUIDemo/Views/BarView.axaml
  20. 28
      samples/ReactiveUIDemo/Views/BarView.axaml.cs
  21. 16
      samples/ReactiveUIDemo/Views/FooView.axaml
  22. 28
      samples/ReactiveUIDemo/Views/FooView.axaml.cs
  23. 2
      samples/RenderDemo/Pages/TextFormatterPage.axaml.cs
  24. 5
      samples/interop/Direct3DInteropSample/App.paml
  25. 21
      samples/interop/Direct3DInteropSample/App.paml.cs
  26. 32
      samples/interop/Direct3DInteropSample/Direct3DInteropSample.csproj
  27. 283
      samples/interop/Direct3DInteropSample/MainWindow.cs
  28. 14
      samples/interop/Direct3DInteropSample/MainWindow.paml
  29. 45
      samples/interop/Direct3DInteropSample/MainWindowViewModel.cs
  30. 47
      samples/interop/Direct3DInteropSample/MiniCube.fx
  31. 16
      samples/interop/Direct3DInteropSample/Program.cs
  32. 2
      src/Android/Avalonia.Android/AndroidInputMethod.cs
  33. 9
      src/Android/Avalonia.Android/AndroidPlatform.cs
  34. 32
      src/Android/Avalonia.Android/OpenGL/GlPlatformSurface.cs
  35. 30
      src/Android/Avalonia.Android/OpenGL/GlRenderTarget.cs
  36. 2
      src/Android/Avalonia.Android/Platform/SkiaPlatform/AndroidFramebuffer.cs
  37. 18
      src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs
  38. 3
      src/Avalonia.Base/Animation/Animators/Animator`1.cs
  39. 11
      src/Avalonia.Base/Animation/Cue.cs
  40. 9
      src/Avalonia.Base/CornerRadius.cs
  41. 2
      src/Avalonia.Base/Data/BindingValue.cs
  42. 2
      src/Avalonia.Base/Data/Core/ExpressionNode.cs
  43. 2
      src/Avalonia.Base/Data/Core/ExpressionObserver.cs
  44. 152
      src/Avalonia.Base/Input/GestureRecognizers/PullGestureRecognizer.cs
  45. 8
      src/Avalonia.Base/Input/Gestures.cs
  46. 5
      src/Avalonia.Base/Input/InputElement.cs
  47. 4
      src/Avalonia.Base/Input/PointerPoint.cs
  48. 43
      src/Avalonia.Base/Input/PullGestureEventArgs.cs
  49. 2
      src/Avalonia.Base/Input/Raw/RawPointerEventArgs.cs
  50. 2
      src/Avalonia.Base/Input/TextInput/ITextInputMethodClient.cs
  51. 2
      src/Avalonia.Base/Logging/ParametrizedLogger.cs
  52. 2
      src/Avalonia.Base/Matrix.cs
  53. 17
      src/Avalonia.Base/Media/BoxShadow.cs
  54. 12
      src/Avalonia.Base/Media/BoxShadows.cs
  55. 8
      src/Avalonia.Base/Media/DashStyle.cs
  56. 2
      src/Avalonia.Base/Media/DrawingContext.cs
  57. 2
      src/Avalonia.Base/Media/FontMetrics.cs
  58. 10
      src/Avalonia.Base/Media/FormattedText.cs
  59. 4
      src/Avalonia.Base/Media/Geometry.cs
  60. 2
      src/Avalonia.Base/Media/GeometryDrawing.cs
  61. 2
      src/Avalonia.Base/Media/GlyphMetrics.cs
  62. 419
      src/Avalonia.Base/Media/GlyphRun.cs
  63. 2
      src/Avalonia.Base/Media/GlyphRunDrawing.cs
  64. 20
      src/Avalonia.Base/Media/GlyphRunMetrics.cs
  65. 2
      src/Avalonia.Base/Media/IDashStyle.cs
  66. 2
      src/Avalonia.Base/Media/ImageDrawing.cs
  67. 4
      src/Avalonia.Base/Media/Imaging/CroppedBitmap.cs
  68. 2
      src/Avalonia.Base/Media/Imaging/RenderTargetBitmap.cs
  69. 4
      src/Avalonia.Base/Media/Immutable/ImmutableDashStyle.cs
  70. 2
      src/Avalonia.Base/Media/TextCollapsingCreateInfo.cs
  71. 293
      src/Avalonia.Base/Media/TextFormatting/CharacterBufferRange.cs
  72. 115
      src/Avalonia.Base/Media/TextFormatting/CharacterBufferReference.cs
  73. 12
      src/Avalonia.Base/Media/TextFormatting/FormattedTextSource.cs
  74. 19
      src/Avalonia.Base/Media/TextFormatting/InterWordJustification.cs
  75. 20
      src/Avalonia.Base/Media/TextFormatting/ShapeableTextCharacters.cs
  76. 33
      src/Avalonia.Base/Media/TextFormatting/ShapedBuffer.cs
  77. 19
      src/Avalonia.Base/Media/TextFormatting/ShapedTextCharacters.cs
  78. 4
      src/Avalonia.Base/Media/TextFormatting/SplitResult.cs
  79. 129
      src/Avalonia.Base/Media/TextFormatting/TextCharacters.cs
  80. 102
      src/Avalonia.Base/Media/TextFormatting/TextEllipsisHelper.cs
  81. 4
      src/Avalonia.Base/Media/TextFormatting/TextEndOfLine.cs
  82. 100
      src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs
  83. 2
      src/Avalonia.Base/Media/TextFormatting/TextLayout.cs
  84. 5
      src/Avalonia.Base/Media/TextFormatting/TextLeadingPrefixCharacterEllipsis.cs
  85. 362
      src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs
  86. 8
      src/Avalonia.Base/Media/TextFormatting/TextLineMetrics.cs
  87. 6
      src/Avalonia.Base/Media/TextFormatting/TextMetrics.cs
  88. 2
      src/Avalonia.Base/Media/TextFormatting/TextRange.cs
  89. 11
      src/Avalonia.Base/Media/TextFormatting/TextRun.cs
  90. 2
      src/Avalonia.Base/Media/TextFormatting/TextRunBounds.cs
  91. 11
      src/Avalonia.Base/Media/TextFormatting/TextShaper.cs
  92. 2
      src/Avalonia.Base/Media/TextFormatting/TextShaperOptions.cs
  93. 3
      src/Avalonia.Base/Media/TextFormatting/TextTrailingCharacterEllipsis.cs
  94. 2
      src/Avalonia.Base/Media/TextFormatting/TextTrailingWordEllipsis.cs
  95. 3
      src/Avalonia.Base/Media/TextFormatting/Unicode/BiDiData.cs
  96. 11
      src/Avalonia.Base/Media/TextFormatting/Unicode/Codepoint.cs
  97. 6
      src/Avalonia.Base/Media/TextFormatting/Unicode/CodepointEnumerator.cs
  98. 8
      src/Avalonia.Base/Media/TextFormatting/Unicode/Grapheme.cs
  99. 12
      src/Avalonia.Base/Media/TextFormatting/Unicode/GraphemeEnumerator.cs
  100. 2
      src/Avalonia.Base/Media/TextFormatting/Unicode/LineBreak.cs

6
.editorconfig

@ -140,16 +140,22 @@ dotnet_analyzer_diagnostic.category-Performance.severity = none #error - Uncomme
# CS1591: Missing XML comment for publicly visible type or member # CS1591: Missing XML comment for publicly visible type or member
dotnet_diagnostic.CS1591.severity = suggestion dotnet_diagnostic.CS1591.severity = suggestion
# CS0162: Remove unreachable code
dotnet_diagnostic.CS0162.severity = error
# CA1304: Specify CultureInfo # CA1304: Specify CultureInfo
dotnet_diagnostic.CA1304.severity = warning dotnet_diagnostic.CA1304.severity = warning
# CA1802: Use literals where appropriate # CA1802: Use literals where appropriate
dotnet_diagnostic.CA1802.severity = warning dotnet_diagnostic.CA1802.severity = warning
# CA1815: Override equals and operator equals on value types
dotnet_diagnostic.CA1815.severity = warning
# CA1820: Test for empty strings using string length # CA1820: Test for empty strings using string length
dotnet_diagnostic.CA1820.severity = warning dotnet_diagnostic.CA1820.severity = warning
# CA1821: Remove empty finalizers # CA1821: Remove empty finalizers
dotnet_diagnostic.CA1821.severity = warning dotnet_diagnostic.CA1821.severity = warning
# CA1822: Mark members as static # CA1822: Mark members as static
dotnet_diagnostic.CA1822.severity = suggestion dotnet_diagnostic.CA1822.severity = suggestion
# CA1823: Avoid unused private fields
dotnet_diagnostic.CA1823.severity = warning
dotnet_code_quality.CA1822.api_surface = private, internal dotnet_code_quality.CA1822.api_surface = private, internal
# CA1825: Avoid zero-length array allocations # CA1825: Avoid zero-length array allocations
dotnet_diagnostic.CA1825.severity = warning dotnet_diagnostic.CA1825.severity = warning

5
.ncrunch/ReactiveUIDemo.v3.ncrunchproject

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

1
Avalonia.Desktop.slnf

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

18
Avalonia.sln

@ -136,8 +136,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Linux", "Linux", "{86C53C40
EndProject EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.LinuxFramebuffer", "src\Linux\Avalonia.LinuxFramebuffer\Avalonia.LinuxFramebuffer.csproj", "{854568D5-13D1-4B4F-B50D-534DC7EFD3C9}" Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.LinuxFramebuffer", "src\Linux\Avalonia.LinuxFramebuffer\Avalonia.LinuxFramebuffer.csproj", "{854568D5-13D1-4B4F-B50D-534DC7EFD3C9}"
EndProject EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Direct3DInteropSample", "samples\interop\Direct3DInteropSample\Direct3DInteropSample.csproj", "{638580B0-7910-40EF-B674-DCB34DA308CD}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Win32.Interop", "src\Windows\Avalonia.Win32.Interop\Avalonia.Win32.Interop.csproj", "{CBC4FF2F-92D4-420B-BE21-9FE0B930B04E}" Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Win32.Interop", "src\Windows\Avalonia.Win32.Interop\Avalonia.Win32.Interop.csproj", "{CBC4FF2F-92D4-420B-BE21-9FE0B930B04E}"
EndProject EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Skia.RenderTests", "tests\Avalonia.Skia.RenderTests\Avalonia.Skia.RenderTests.csproj", "{E1582370-37B3-403C-917F-8209551B1634}" Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Skia.RenderTests", "tests\Avalonia.Skia.RenderTests\Avalonia.Skia.RenderTests.csproj", "{E1582370-37B3-403C-917F-8209551B1634}"
@ -228,9 +226,11 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Browser", "src\Bro
EndProject EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Browser.Blazor", "src\Browser\Avalonia.Browser.Blazor\Avalonia.Browser.Blazor.csproj", "{47F8530C-F19B-4B1A-B4D6-EB231522AE5D}" Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Browser.Blazor", "src\Browser\Avalonia.Browser.Blazor\Avalonia.Browser.Blazor.csproj", "{47F8530C-F19B-4B1A-B4D6-EB231522AE5D}"
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ControlCatalog.Browser", "samples\ControlCatalog.Browser\ControlCatalog.Browser.csproj", "{15B93A4C-1B46-43F6-B534-7B25B6E99932}" Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ControlCatalog.Browser", "samples\ControlCatalog.Browser\ControlCatalog.Browser.csproj", "{15B93A4C-1B46-43F6-B534-7B25B6E99932}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ControlCatalog.Browser.Blazor", "samples\ControlCatalog.Browser.Blazor\ControlCatalog.Browser.Blazor.csproj", "{90B08091-9BBD-4362-B712-E9F2CC62B218}"
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ControlCatalog.Browser.Blazor", "samples\ControlCatalog.Browser.Blazor\ControlCatalog.Browser.Blazor.csproj", "{90B08091-9BBD-4362-B712-E9F2CC62B218}" Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ReactiveUIDemo", "samples\ReactiveUIDemo\ReactiveUIDemo.csproj", "{75C47156-C5D8-44BC-A5A7-E8657C2248D6}"
EndProject EndProject
Global Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution
@ -364,10 +364,6 @@ Global
{854568D5-13D1-4B4F-B50D-534DC7EFD3C9}.Debug|Any CPU.Build.0 = Debug|Any CPU {854568D5-13D1-4B4F-B50D-534DC7EFD3C9}.Debug|Any CPU.Build.0 = Debug|Any CPU
{854568D5-13D1-4B4F-B50D-534DC7EFD3C9}.Release|Any CPU.ActiveCfg = Release|Any CPU {854568D5-13D1-4B4F-B50D-534DC7EFD3C9}.Release|Any CPU.ActiveCfg = Release|Any CPU
{854568D5-13D1-4B4F-B50D-534DC7EFD3C9}.Release|Any CPU.Build.0 = Release|Any CPU {854568D5-13D1-4B4F-B50D-534DC7EFD3C9}.Release|Any CPU.Build.0 = Release|Any CPU
{638580B0-7910-40EF-B674-DCB34DA308CD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{638580B0-7910-40EF-B674-DCB34DA308CD}.Debug|Any CPU.Build.0 = Debug|Any CPU
{638580B0-7910-40EF-B674-DCB34DA308CD}.Release|Any CPU.ActiveCfg = Release|Any CPU
{638580B0-7910-40EF-B674-DCB34DA308CD}.Release|Any CPU.Build.0 = Release|Any CPU
{CBC4FF2F-92D4-420B-BE21-9FE0B930B04E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {CBC4FF2F-92D4-420B-BE21-9FE0B930B04E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{CBC4FF2F-92D4-420B-BE21-9FE0B930B04E}.Debug|Any CPU.Build.0 = Debug|Any CPU {CBC4FF2F-92D4-420B-BE21-9FE0B930B04E}.Debug|Any CPU.Build.0 = Debug|Any CPU
{CBC4FF2F-92D4-420B-BE21-9FE0B930B04E}.Release|Any CPU.ActiveCfg = Release|Any CPU {CBC4FF2F-92D4-420B-BE21-9FE0B930B04E}.Release|Any CPU.ActiveCfg = Release|Any CPU
@ -543,6 +539,10 @@ Global
{90B08091-9BBD-4362-B712-E9F2CC62B218}.Debug|Any CPU.Build.0 = Debug|Any CPU {90B08091-9BBD-4362-B712-E9F2CC62B218}.Debug|Any CPU.Build.0 = Debug|Any CPU
{90B08091-9BBD-4362-B712-E9F2CC62B218}.Release|Any CPU.ActiveCfg = Release|Any CPU {90B08091-9BBD-4362-B712-E9F2CC62B218}.Release|Any CPU.ActiveCfg = Release|Any CPU
{90B08091-9BBD-4362-B712-E9F2CC62B218}.Release|Any CPU.Build.0 = Release|Any CPU {90B08091-9BBD-4362-B712-E9F2CC62B218}.Release|Any CPU.Build.0 = Release|Any CPU
{75C47156-C5D8-44BC-A5A7-E8657C2248D6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{75C47156-C5D8-44BC-A5A7-E8657C2248D6}.Debug|Any CPU.Build.0 = Debug|Any CPU
{75C47156-C5D8-44BC-A5A7-E8657C2248D6}.Release|Any CPU.ActiveCfg = Release|Any CPU
{75C47156-C5D8-44BC-A5A7-E8657C2248D6}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection EndGlobalSection
GlobalSection(SolutionProperties) = preSolution GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE HideSolutionNode = FALSE
@ -574,7 +574,6 @@ Global
{7D2D3083-71DD-4CC9-8907-39A0D86FB322} = {3743B0F2-CC41-4F14-A8C8-267F579BF91E} {7D2D3083-71DD-4CC9-8907-39A0D86FB322} = {3743B0F2-CC41-4F14-A8C8-267F579BF91E}
{39D7B147-1A5B-47C2-9D01-21FB7C47C4B3} = {9B9E3891-2366-4253-A952-D08BCEB71098} {39D7B147-1A5B-47C2-9D01-21FB7C47C4B3} = {9B9E3891-2366-4253-A952-D08BCEB71098}
{854568D5-13D1-4B4F-B50D-534DC7EFD3C9} = {86C53C40-57AA-45B8-AD42-FAE0EFDF0F2B} {854568D5-13D1-4B4F-B50D-534DC7EFD3C9} = {86C53C40-57AA-45B8-AD42-FAE0EFDF0F2B}
{638580B0-7910-40EF-B674-DCB34DA308CD} = {A0CC0258-D18C-4AB3-854F-7101680FC3F9}
{CBC4FF2F-92D4-420B-BE21-9FE0B930B04E} = {B39A8919-9F95-48FE-AD7B-76E08B509888} {CBC4FF2F-92D4-420B-BE21-9FE0B930B04E} = {B39A8919-9F95-48FE-AD7B-76E08B509888}
{E1582370-37B3-403C-917F-8209551B1634} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B} {E1582370-37B3-403C-917F-8209551B1634} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B}
{E2999E4A-9086-401F-898C-AEB0AD38E676} = {9B9E3891-2366-4253-A952-D08BCEB71098} {E2999E4A-9086-401F-898C-AEB0AD38E676} = {9B9E3891-2366-4253-A952-D08BCEB71098}
@ -607,6 +606,7 @@ Global
{47F8530C-F19B-4B1A-B4D6-EB231522AE5D} = {86A3F706-DC3C-43C6-BE1B-B98F5BAAA268} {47F8530C-F19B-4B1A-B4D6-EB231522AE5D} = {86A3F706-DC3C-43C6-BE1B-B98F5BAAA268}
{15B93A4C-1B46-43F6-B534-7B25B6E99932} = {9B9E3891-2366-4253-A952-D08BCEB71098} {15B93A4C-1B46-43F6-B534-7B25B6E99932} = {9B9E3891-2366-4253-A952-D08BCEB71098}
{90B08091-9BBD-4362-B712-E9F2CC62B218} = {9B9E3891-2366-4253-A952-D08BCEB71098} {90B08091-9BBD-4362-B712-E9F2CC62B218} = {9B9E3891-2366-4253-A952-D08BCEB71098}
{75C47156-C5D8-44BC-A5A7-E8657C2248D6} = {9B9E3891-2366-4253-A952-D08BCEB71098}
EndGlobalSection EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {87366D66-1391-4D90-8999-95A620AD786A} SolutionGuid = {87366D66-1391-4D90-8999-95A620AD786A}

3
samples/ControlCatalog/MainView.xaml

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

2
samples/ControlCatalog/Pages/ImagePage.xaml.cs

@ -70,7 +70,7 @@ namespace ControlCatalog.Pages
3 => new PixelRect(new PixelPoint(bitmapWidth - cropSize.Width, 0), cropSize), 3 => new PixelRect(new PixelPoint(bitmapWidth - cropSize.Width, 0), cropSize),
4 => new PixelRect(new PixelPoint(0, bitmapHeight - cropSize.Height), cropSize), 4 => new PixelRect(new PixelPoint(0, bitmapHeight - cropSize.Height), cropSize),
5 => new PixelRect(new PixelPoint(bitmapWidth - cropSize.Width, bitmapHeight - cropSize.Height), cropSize), 5 => new PixelRect(new PixelPoint(bitmapWidth - cropSize.Width, bitmapHeight - cropSize.Height), cropSize),
_ => PixelRect.Empty _ => default
}; };
} }

27
samples/ControlCatalog/Pages/RefreshContainerPage.axaml

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

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

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

26
samples/ControlCatalog/ViewModels/RefreshContainerViewModel.cs

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

8
samples/ReactiveUIDemo/App.axaml

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

37
samples/ReactiveUIDemo/App.axaml.cs

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

19
samples/ReactiveUIDemo/MainWindow.axaml

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

22
samples/ReactiveUIDemo/MainWindow.axaml.cs

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

28
samples/ReactiveUIDemo/ReactiveUIDemo.csproj

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

11
samples/ReactiveUIDemo/ViewModels/BarViewModel.cs

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

11
samples/ReactiveUIDemo/ViewModels/FooViewModel.cs

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

9
samples/ReactiveUIDemo/ViewModels/MainWindowViewModel.cs

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

21
samples/ReactiveUIDemo/ViewModels/RoutedViewHostPageViewModel.cs

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

16
samples/ReactiveUIDemo/Views/BarView.axaml

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

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

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

16
samples/ReactiveUIDemo/Views/FooView.axaml

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

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

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

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

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

5
samples/interop/Direct3DInteropSample/App.paml

@ -1,5 +0,0 @@
<Application xmlns="https://github.com/avaloniaui">
<Application.Styles>
<SimpleTheme Mode="Light" />
</Application.Styles>
</Application>

21
samples/interop/Direct3DInteropSample/App.paml.cs

@ -1,21 +0,0 @@
using Avalonia;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Markup.Xaml;
namespace Direct3DInteropSample
{
public class App : Application
{
public override void Initialize()
{
AvaloniaXamlLoader.Load(this);
}
public override void OnFrameworkInitializationCompleted()
{
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
desktop.MainWindow = new MainWindow();
base.OnFrameworkInitializationCompleted();
}
}
}

32
samples/interop/Direct3DInteropSample/Direct3DInteropSample.csproj

@ -1,32 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net461</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="SharpDX.Mathematics" Version="4.0.1" />
<PackageReference Include="SharpDX.D3DCompiler" Version="4.0.1" />
<Compile Update="**\*.paml.cs">
<DependentUpon>%(Filename)</DependentUpon>
</Compile>
<EmbeddedResource Include="**\*.paml">
<SubType>Designer</SubType>
</EmbeddedResource>
</ItemGroup>
<ItemGroup>
<None Remove="MiniCube.fx" />
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="MiniCube.fx">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</EmbeddedResource>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\src\Avalonia.Themes.Simple\Avalonia.Themes.Simple.csproj" />
<ProjectReference Include="..\..\..\src\Windows\Avalonia.Direct2D1\Avalonia.Direct2D1.csproj" />
<ProjectReference Include="..\..\..\src\Windows\Avalonia.Win32\Avalonia.Win32.csproj" />
<ProjectReference Include="..\..\MiniMvvm\MiniMvvm.csproj" />
</ItemGroup>
<Import Project="..\..\..\build\Rx.props" />
<Import Project="..\..\..\build\ReferenceCoreLibraries.props" />
</Project>

283
samples/interop/Direct3DInteropSample/MainWindow.cs

@ -1,283 +0,0 @@
using System;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Direct2D1;
using Avalonia.Direct2D1.Media;
using Avalonia.Markup.Xaml;
using Avalonia.Platform;
using Avalonia.Rendering;
using SharpDX;
using SharpDX.D3DCompiler;
using SharpDX.Direct2D1;
using SharpDX.Direct3D;
using SharpDX.Direct3D11;
using SharpDX.DXGI;
using AlphaMode = SharpDX.Direct2D1.AlphaMode;
using Buffer = SharpDX.Direct3D11.Buffer;
using DeviceContext = SharpDX.Direct2D1.DeviceContext;
using Factory2 = SharpDX.DXGI.Factory2;
using InputElement = SharpDX.Direct3D11.InputElement;
using Matrix = SharpDX.Matrix;
using PixelFormat = SharpDX.Direct2D1.PixelFormat;
using Resource = SharpDX.Direct3D11.Resource;
namespace Direct3DInteropSample
{
public class MainWindow : Window
{
Texture2D _backBuffer;
RenderTargetView _renderView;
Texture2D _depthBuffer;
DepthStencilView _depthView;
private readonly SwapChain _swapChain;
private SwapChainDescription1 _desc;
private Matrix _proj = Matrix.Identity;
private readonly Matrix _view;
private Buffer _contantBuffer;
private DeviceContext _deviceContext;
private readonly MainWindowViewModel _model;
public MainWindow()
{
DataContext = _model = new MainWindowViewModel();
_desc = new SwapChainDescription1()
{
BufferCount = 1,
Width = (int)ClientSize.Width,
Height = (int)ClientSize.Height,
Format = Format.R8G8B8A8_UNorm,
SampleDescription = new SampleDescription(1, 0),
SwapEffect = SwapEffect.Discard,
Usage = Usage.RenderTargetOutput
};
using (var factory = Direct2D1Platform.DxgiDevice.Adapter.GetParent<Factory2>())
{
_swapChain = new SwapChain1(factory, Direct2D1Platform.DxgiDevice, PlatformImpl?.Handle.Handle ?? IntPtr.Zero, ref _desc);
}
_deviceContext = new DeviceContext(Direct2D1Platform.Direct2D1Device, DeviceContextOptions.None)
{
DotsPerInch = new Size2F(96, 96)
};
CreateMesh();
_view = Matrix.LookAtLH(new Vector3(0, 0, -5), new Vector3(0, 0, 0), Vector3.UnitY);
this.GetObservable(ClientSizeProperty).Subscribe(Resize);
Resize(ClientSize);
AvaloniaXamlLoader.Load(this);
Background = Avalonia.Media.Brushes.Transparent;
}
protected override void HandlePaint(Rect rect)
{
var viewProj = Matrix.Multiply(_view, _proj);
var context = Direct2D1Platform.Direct3D11Device.ImmediateContext;
// Clear views
context.ClearDepthStencilView(_depthView, DepthStencilClearFlags.Depth, 1.0f, 0);
context.ClearRenderTargetView(_renderView, Color.White);
// Update WorldViewProj Matrix
var worldViewProj = Matrix.RotationX((float)_model.RotationX) * Matrix.RotationY((float)_model.RotationY)
* Matrix.RotationZ((float)_model.RotationZ)
* Matrix.Scaling((float)_model.Zoom)
* viewProj;
worldViewProj.Transpose();
context.UpdateSubresource(ref worldViewProj, _contantBuffer);
// Draw the cube
context.Draw(36, 0);
base.HandlePaint(rect);
// Present!
_swapChain.Present(0, PresentFlags.None);
}
private void CreateMesh()
{
var device = Direct2D1Platform.Direct3D11Device;
// Compile Vertex and Pixel shaders
var vertexShaderByteCode = ShaderBytecode.CompileFromFile("MiniCube.fx", "VS", "vs_4_0");
var vertexShader = new VertexShader(device, vertexShaderByteCode);
var pixelShaderByteCode = ShaderBytecode.CompileFromFile("MiniCube.fx", "PS", "ps_4_0");
var pixelShader = new PixelShader(device, pixelShaderByteCode);
var signature = ShaderSignature.GetInputSignature(vertexShaderByteCode);
var inputElements = new[]
{
new InputElement("POSITION", 0, Format.R32G32B32A32_Float, 0, 0),
new InputElement("COLOR", 0, Format.R32G32B32A32_Float, 16, 0)
};
// Layout from VertexShader input signature
var layout = new InputLayout(
device,
signature,
inputElements);
// Instantiate Vertex buffer from vertex data
var vertices = Buffer.Create(
device,
BindFlags.VertexBuffer,
new[]
{
new Vector4(-1.0f, -1.0f, -1.0f, 1.0f), new Vector4(1.0f, 0.0f, 0.0f, 1.0f), // Front
new Vector4(-1.0f, 1.0f, -1.0f, 1.0f), new Vector4(1.0f, 0.0f, 0.0f, 1.0f),
new Vector4( 1.0f, 1.0f, -1.0f, 1.0f), new Vector4(1.0f, 0.0f, 0.0f, 1.0f),
new Vector4(-1.0f, -1.0f, -1.0f, 1.0f), new Vector4(1.0f, 0.0f, 0.0f, 1.0f),
new Vector4( 1.0f, 1.0f, -1.0f, 1.0f), new Vector4(1.0f, 0.0f, 0.0f, 1.0f),
new Vector4( 1.0f, -1.0f, -1.0f, 1.0f), new Vector4(1.0f, 0.0f, 0.0f, 1.0f),
new Vector4(-1.0f, -1.0f, 1.0f, 1.0f), new Vector4(0.0f, 1.0f, 0.0f, 1.0f), // BACK
new Vector4( 1.0f, 1.0f, 1.0f, 1.0f), new Vector4(0.0f, 1.0f, 0.0f, 1.0f),
new Vector4(-1.0f, 1.0f, 1.0f, 1.0f), new Vector4(0.0f, 1.0f, 0.0f, 1.0f),
new Vector4(-1.0f, -1.0f, 1.0f, 1.0f), new Vector4(0.0f, 1.0f, 0.0f, 1.0f),
new Vector4( 1.0f, -1.0f, 1.0f, 1.0f), new Vector4(0.0f, 1.0f, 0.0f, 1.0f),
new Vector4( 1.0f, 1.0f, 1.0f, 1.0f), new Vector4(0.0f, 1.0f, 0.0f, 1.0f),
new Vector4(-1.0f, 1.0f, -1.0f, 1.0f), new Vector4(0.0f, 0.0f, 1.0f, 1.0f), // Top
new Vector4(-1.0f, 1.0f, 1.0f, 1.0f), new Vector4(0.0f, 0.0f, 1.0f, 1.0f),
new Vector4( 1.0f, 1.0f, 1.0f, 1.0f), new Vector4(0.0f, 0.0f, 1.0f, 1.0f),
new Vector4(-1.0f, 1.0f, -1.0f, 1.0f), new Vector4(0.0f, 0.0f, 1.0f, 1.0f),
new Vector4( 1.0f, 1.0f, 1.0f, 1.0f), new Vector4(0.0f, 0.0f, 1.0f, 1.0f),
new Vector4( 1.0f, 1.0f, -1.0f, 1.0f), new Vector4(0.0f, 0.0f, 1.0f, 1.0f),
new Vector4(-1.0f, -1.0f, -1.0f, 1.0f), new Vector4(1.0f, 1.0f, 0.0f, 1.0f), // Bottom
new Vector4( 1.0f, -1.0f, 1.0f, 1.0f), new Vector4(1.0f, 1.0f, 0.0f, 1.0f),
new Vector4(-1.0f, -1.0f, 1.0f, 1.0f), new Vector4(1.0f, 1.0f, 0.0f, 1.0f),
new Vector4(-1.0f, -1.0f, -1.0f, 1.0f), new Vector4(1.0f, 1.0f, 0.0f, 1.0f),
new Vector4( 1.0f, -1.0f, -1.0f, 1.0f), new Vector4(1.0f, 1.0f, 0.0f, 1.0f),
new Vector4( 1.0f, -1.0f, 1.0f, 1.0f), new Vector4(1.0f, 1.0f, 0.0f, 1.0f),
new Vector4(-1.0f, -1.0f, -1.0f, 1.0f), new Vector4(1.0f, 0.0f, 1.0f, 1.0f), // Left
new Vector4(-1.0f, -1.0f, 1.0f, 1.0f), new Vector4(1.0f, 0.0f, 1.0f, 1.0f),
new Vector4(-1.0f, 1.0f, 1.0f, 1.0f), new Vector4(1.0f, 0.0f, 1.0f, 1.0f),
new Vector4(-1.0f, -1.0f, -1.0f, 1.0f), new Vector4(1.0f, 0.0f, 1.0f, 1.0f),
new Vector4(-1.0f, 1.0f, 1.0f, 1.0f), new Vector4(1.0f, 0.0f, 1.0f, 1.0f),
new Vector4(-1.0f, 1.0f, -1.0f, 1.0f), new Vector4(1.0f, 0.0f, 1.0f, 1.0f),
new Vector4( 1.0f, -1.0f, -1.0f, 1.0f), new Vector4(0.0f, 1.0f, 1.0f, 1.0f), // Right
new Vector4( 1.0f, 1.0f, 1.0f, 1.0f), new Vector4(0.0f, 1.0f, 1.0f, 1.0f),
new Vector4( 1.0f, -1.0f, 1.0f, 1.0f), new Vector4(0.0f, 1.0f, 1.0f, 1.0f),
new Vector4( 1.0f, -1.0f, -1.0f, 1.0f), new Vector4(0.0f, 1.0f, 1.0f, 1.0f),
new Vector4( 1.0f, 1.0f, -1.0f, 1.0f), new Vector4(0.0f, 1.0f, 1.0f, 1.0f),
new Vector4( 1.0f, 1.0f, 1.0f, 1.0f), new Vector4(0.0f, 1.0f, 1.0f, 1.0f),
});
// Create Constant Buffer
_contantBuffer = new Buffer(device, Utilities.SizeOf<Matrix>(), ResourceUsage.Default, BindFlags.ConstantBuffer, CpuAccessFlags.None, ResourceOptionFlags.None, 0);
var context = Direct2D1Platform.Direct3D11Device.ImmediateContext;
// Prepare All the stages
context.InputAssembler.InputLayout = layout;
context.InputAssembler.PrimitiveTopology = PrimitiveTopology.TriangleList;
context.InputAssembler.SetVertexBuffers(0, new VertexBufferBinding(vertices, Utilities.SizeOf<Vector4>() * 2, 0));
context.VertexShader.SetConstantBuffer(0, _contantBuffer);
context.VertexShader.Set(vertexShader);
context.PixelShader.Set(pixelShader);
}
private void Resize(Size size)
{
Utilities.Dispose(ref _deviceContext);
Utilities.Dispose(ref _backBuffer);
Utilities.Dispose(ref _renderView);
Utilities.Dispose(ref _depthBuffer);
Utilities.Dispose(ref _depthView);
var context = Direct2D1Platform.Direct3D11Device.ImmediateContext;
// Resize the backbuffer
_swapChain.ResizeBuffers(0, 0, 0, Format.Unknown, SwapChainFlags.None);
// Get the backbuffer from the swapchain
_backBuffer = Resource.FromSwapChain<Texture2D>(_swapChain, 0);
// Renderview on the backbuffer
_renderView = new RenderTargetView(Direct2D1Platform.Direct3D11Device, _backBuffer);
// Create the depth buffer
_depthBuffer = new Texture2D(
Direct2D1Platform.Direct3D11Device,
new Texture2DDescription()
{
Format = Format.D32_Float_S8X24_UInt,
ArraySize = 1,
MipLevels = 1,
Width = (int)size.Width,
Height = (int)size.Height,
SampleDescription = new SampleDescription(1, 0),
Usage = ResourceUsage.Default,
BindFlags = BindFlags.DepthStencil,
CpuAccessFlags = CpuAccessFlags.None,
OptionFlags = ResourceOptionFlags.None
});
// Create the depth buffer view
_depthView = new DepthStencilView(Direct2D1Platform.Direct3D11Device, _depthBuffer);
// Setup targets and viewport for rendering
context.Rasterizer.SetViewport(new Viewport(0, 0, (int)size.Width, (int)size.Height, 0.0f, 1.0f));
context.OutputMerger.SetTargets(_depthView, _renderView);
// Setup new projection matrix with correct aspect ratio
_proj = Matrix.PerspectiveFovLH((float)Math.PI / 4.0f, (float)(size.Width / size.Height), 0.1f, 100.0f);
using (var dxgiBackBuffer = _swapChain.GetBackBuffer<Surface>(0))
{
var renderTarget = new SharpDX.Direct2D1.RenderTarget(
Direct2D1Platform.Direct2D1Factory,
dxgiBackBuffer,
new RenderTargetProperties
{
DpiX = 96,
DpiY = 96,
Type = RenderTargetType.Default,
PixelFormat = new PixelFormat(
Format.Unknown,
AlphaMode.Premultiplied)
});
_deviceContext = renderTarget.QueryInterface<DeviceContext>();
renderTarget.Dispose();
}
}
private class D3DRenderTarget : IRenderTarget
{
private readonly MainWindow _window;
public D3DRenderTarget(MainWindow window)
{
_window = window;
}
public void Dispose()
{
}
public IDrawingContextImpl CreateDrawingContext(IVisualBrushRenderer visualBrushRenderer)
{
return new DrawingContextImpl(visualBrushRenderer, null, _window._deviceContext);
}
}
protected override IRenderTarget CreateRenderTarget() => new D3DRenderTarget(this);
}
}

14
samples/interop/Direct3DInteropSample/MainWindow.paml

@ -1,14 +0,0 @@
<Window xmlns="https://github.com/avaloniaui" Background="White" Title="Avalonia Direct3D Demo">
<Grid ColumnDefinitions="*,Auto" Margin="20">
<StackPanel Grid.Column="1" MinWidth="200">
<TextBlock>Rotation X</TextBlock>
<Slider Value="{Binding RotationX, Mode=TwoWay}" Maximum="10"/>
<TextBlock>Rotation Y</TextBlock>
<Slider Value="{Binding RotationY, Mode=TwoWay}" Maximum="10"/>
<TextBlock>Rotation Z</TextBlock>
<Slider Value="{Binding RotationZ, Mode=TwoWay}" Maximum="10"/>
<TextBlock>Zoom</TextBlock>
<Slider Value="{Binding Zoom, Mode=TwoWay}" Maximum="3" Minimum="0.5"/>
</StackPanel>
</Grid>
</Window>

45
samples/interop/Direct3DInteropSample/MainWindowViewModel.cs

@ -1,45 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using MiniMvvm;
namespace Direct3DInteropSample
{
public class MainWindowViewModel : ViewModelBase
{
private double _rotationX;
public double RotationX
{
get { return _rotationX; }
set { this.RaiseAndSetIfChanged(ref _rotationX, value); }
}
private double _rotationY = 1;
public double RotationY
{
get { return _rotationY; }
set { this.RaiseAndSetIfChanged(ref _rotationY, value); }
}
private double _rotationZ = 2;
public double RotationZ
{
get { return _rotationZ; }
set { this.RaiseAndSetIfChanged(ref _rotationZ, value); }
}
private double _zoom = 1;
public double Zoom
{
get { return _zoom; }
set { this.RaiseAndSetIfChanged(ref _zoom, value); }
}
}
}

47
samples/interop/Direct3DInteropSample/MiniCube.fx

@ -1,47 +0,0 @@
// Copyright (c) 2010-2013 SharpDX - Alexandre Mutel
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
struct VS_IN
{
float4 pos : POSITION;
float4 col : COLOR;
};
struct PS_IN
{
float4 pos : SV_POSITION;
float4 col : COLOR;
};
float4x4 worldViewProj;
PS_IN VS( VS_IN input )
{
PS_IN output = (PS_IN)0;
output.pos = mul(input.pos, worldViewProj);
output.col = input.col;
return output;
}
float4 PS( PS_IN input ) : SV_Target
{
return input.col;
}

16
samples/interop/Direct3DInteropSample/Program.cs

@ -1,16 +0,0 @@
using Avalonia;
namespace Direct3DInteropSample
{
class Program
{
public static AppBuilder BuildAvaloniaApp()
=> AppBuilder.Configure<App>()
.With(new Win32PlatformOptions { UseDeferredRendering = false })
.UseWin32()
.UseDirect2D1();
public static int Main(string[] args)
=> BuildAvaloniaApp().StartWithClassicDesktopLifetime(args);
}
}

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

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

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

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

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

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

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

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

2
src/Android/Avalonia.Android/Platform/SkiaPlatform/AndroidFramebuffer.cs

@ -96,12 +96,14 @@ namespace Avalonia.Android.Platform.SkiaPlatform
public IntPtr bits; public IntPtr bits;
// Do not touch. // Do not touch.
#pragma warning disable CA1823 // Avoid unused private fields
uint reserved1; uint reserved1;
uint reserved2; uint reserved2;
uint reserved3; uint reserved3;
uint reserved4; uint reserved4;
uint reserved5; uint reserved5;
uint reserved6; uint reserved6;
#pragma warning restore CA1823 // Avoid unused private fields
} }
} }
} }

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

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

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

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

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

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

9
src/Avalonia.Base/CornerRadius.cs

@ -61,9 +61,13 @@ namespace Avalonia
public double BottomLeft { get; } public double BottomLeft { get; }
/// <summary> /// <summary>
/// Gets a value indicating whether all corner radii are set to 0. /// Gets a value indicating whether the instance has default values (all corner radii are set to 0).
/// </summary> /// </summary>
public bool IsEmpty => TopLeft.Equals(0) && IsUniform; public bool IsDefault => TopLeft == 0 && TopRight == 0 && BottomLeft == 0 && BottomRight == 0;
/// <inheritdoc cref="IsDefault"/>
[Obsolete("Use IsDefault instead.")]
public bool IsEmpty => IsDefault;
/// <summary> /// <summary>
/// Gets a value indicating whether all corner radii are equal. /// Gets a value indicating whether all corner radii are equal.
@ -79,7 +83,6 @@ namespace Avalonia
{ {
// ReSharper disable CompareOfFloatsByEqualityOperator // ReSharper disable CompareOfFloatsByEqualityOperator
return TopLeft == other.TopLeft && return TopLeft == other.TopLeft &&
TopRight == other.TopRight && TopRight == other.TopRight &&
BottomRight == other.BottomRight && BottomRight == other.BottomRight &&
BottomLeft == other.BottomLeft; BottomLeft == other.BottomLeft;

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

@ -80,7 +80,7 @@ namespace Avalonia.Data
/// - For an unset value, use <see cref="Unset"/> or simply `default` /// - For an unset value, use <see cref="Unset"/> or simply `default`
/// - For other types, call one of the static factory methods /// - For other types, call one of the static factory methods
/// </remarks> /// </remarks>
public readonly struct BindingValue<T> public readonly record struct BindingValue<T>
{ {
private readonly T _value; private readonly T _value;

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

@ -4,8 +4,6 @@ namespace Avalonia.Data.Core
{ {
public abstract class ExpressionNode public abstract class ExpressionNode
{ {
private static readonly object CacheInvalid = new object();
protected static readonly WeakReference<object?> UnsetReference = protected static readonly WeakReference<object?> UnsetReference =
new WeakReference<object?>(AvaloniaProperty.UnsetValue); new WeakReference<object?>(AvaloniaProperty.UnsetValue);

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

@ -49,8 +49,6 @@ namespace Avalonia.Data.Core
new TaskStreamPlugin(), new TaskStreamPlugin(),
new ObservableStreamPlugin(), new ObservableStreamPlugin(),
}; };
private static readonly object UninitializedValue = new object();
private readonly ExpressionNode _node; private readonly ExpressionNode _node;
private object? _root; private object? _root;
private Func<object?>? _rootGetter; private Func<object?>? _rootGetter;

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

2
src/Avalonia.Base/Matrix.cs

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

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

@ -45,7 +45,14 @@ namespace Avalonia.Media
} }
} }
public bool IsEmpty => OffsetX == 0 && OffsetY == 0 && Blur == 0 && Spread == 0; /// <summary>
/// Gets a value indicating whether the instance has default values.
/// </summary>
public bool IsDefault => OffsetX == 0 && OffsetY == 0 && Blur == 0 && Spread == 0;
/// <inheritdoc cref="IsDefault"/>
[Obsolete("Use IsDefault instead.")]
public bool IsEmpty => IsDefault;
private readonly static char[] s_Separator = new char[] { ' ', '\t' }; private readonly static char[] s_Separator = new char[] { ' ', '\t' };
@ -82,7 +89,7 @@ namespace Avalonia.Media
{ {
var sb = StringBuilderCache.Acquire(); var sb = StringBuilderCache.Acquire();
if (IsEmpty) if (IsDefault)
{ {
return "none"; return "none";
} }
@ -171,5 +178,11 @@ namespace Avalonia.Media
public Rect TransformBounds(in Rect rect) public Rect TransformBounds(in Rect rect)
=> IsInset ? rect : rect.Translate(new Vector(OffsetX, OffsetY)).Inflate(Spread + Blur); => IsInset ? rect : rect.Translate(new Vector(OffsetX, OffsetY)).Inflate(Spread + Blur);
public static bool operator ==(BoxShadow left, BoxShadow right) =>
left.Equals(right);
public static bool operator !=(BoxShadow left, BoxShadow right) =>
!(left == right);
} }
} }

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

@ -21,7 +21,7 @@ namespace Avalonia.Media
{ {
_first = shadow; _first = shadow;
_list = null; _list = null;
Count = _first.IsEmpty ? 0 : 1; Count = _first.IsDefault ? 0 : 1;
} }
public BoxShadows(BoxShadow first, BoxShadow[] rest) public BoxShadows(BoxShadow first, BoxShadow[] rest)
@ -62,7 +62,9 @@ namespace Avalonia.Media
} }
[EditorBrowsable(EditorBrowsableState.Never)] [EditorBrowsable(EditorBrowsableState.Never)]
#pragma warning disable CA1815 // Override equals and operator equals on value types
public struct BoxShadowsEnumerator public struct BoxShadowsEnumerator
#pragma warning restore CA1815 // Override equals and operator equals on value types
{ {
private int _index; private int _index;
private BoxShadows _shadows; private BoxShadows _shadows;
@ -118,7 +120,7 @@ namespace Avalonia.Media
get get
{ {
foreach(var boxShadow in this) foreach(var boxShadow in this)
if (!boxShadow.IsEmpty && boxShadow.IsInset) if (!boxShadow.IsDefault && boxShadow.IsInset)
return true; return true;
return false; return false;
} }
@ -149,5 +151,11 @@ namespace Avalonia.Media
return hashCode; return hashCode;
} }
} }
public static bool operator ==(BoxShadows left, BoxShadows right) =>
left.Equals(right);
public static bool operator !=(BoxShadows left, BoxShadows right) =>
!(left == right);
} }
} }

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

@ -17,8 +17,8 @@ namespace Avalonia.Media
/// <summary> /// <summary>
/// Defines the <see cref="Dashes"/> property. /// Defines the <see cref="Dashes"/> property.
/// </summary> /// </summary>
public static readonly StyledProperty<AvaloniaList<double>> DashesProperty = public static readonly StyledProperty<AvaloniaList<double>?> DashesProperty =
AvaloniaProperty.Register<DashStyle, AvaloniaList<double>>(nameof(Dashes)); AvaloniaProperty.Register<DashStyle, AvaloniaList<double>?>(nameof(Dashes));
/// <summary> /// <summary>
/// Defines the <see cref="Offset"/> property. /// Defines the <see cref="Offset"/> property.
@ -83,7 +83,7 @@ namespace Avalonia.Media
/// <summary> /// <summary>
/// Gets or sets the length of alternating dashes and gaps. /// Gets or sets the length of alternating dashes and gaps.
/// </summary> /// </summary>
public AvaloniaList<double> Dashes public AvaloniaList<double>? Dashes
{ {
get => GetValue(DashesProperty); get => GetValue(DashesProperty);
set => SetValue(DashesProperty, value); set => SetValue(DashesProperty, value);
@ -98,7 +98,7 @@ namespace Avalonia.Media
set => SetValue(OffsetProperty, value); set => SetValue(OffsetProperty, value);
} }
IReadOnlyList<double> IDashStyle.Dashes => Dashes; IReadOnlyList<double>? IDashStyle.Dashes => Dashes;
/// <summary> /// <summary>
/// Raised when the dash style changes. /// Raised when the dash style changes.

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

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

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

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

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

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

4
src/Avalonia.Base/Media/Geometry.cs

@ -30,7 +30,7 @@ namespace Avalonia.Media
/// <summary> /// <summary>
/// Gets the geometry's bounding rectangle. /// Gets the geometry's bounding rectangle.
/// </summary> /// </summary>
public Rect Bounds => PlatformImpl?.Bounds ?? Rect.Empty; public Rect Bounds => PlatformImpl?.Bounds ?? default;
/// <summary> /// <summary>
/// Gets the platform-specific implementation of the geometry. /// Gets the platform-specific implementation of the geometry.
@ -84,7 +84,7 @@ namespace Avalonia.Media
/// </summary> /// </summary>
/// <param name="pen">The stroke thickness.</param> /// <param name="pen">The stroke thickness.</param>
/// <returns>The bounding rectangle.</returns> /// <returns>The bounding rectangle.</returns>
public Rect GetRenderBounds(IPen pen) => PlatformImpl?.GetRenderBounds(pen) ?? Rect.Empty; public Rect GetRenderBounds(IPen pen) => PlatformImpl?.GetRenderBounds(pen) ?? default;
/// <summary> /// <summary>
/// Indicates whether the geometry's fill contains the specified point. /// Indicates whether the geometry's fill contains the specified point.

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

@ -69,7 +69,7 @@ namespace Avalonia.Media
public override Rect GetBounds() public override Rect GetBounds()
{ {
IPen pen = Pen ?? s_boundsPen; IPen pen = Pen ?? s_boundsPen;
return Geometry?.GetRenderBounds(pen) ?? Rect.Empty; return Geometry?.GetRenderBounds(pen) ?? default;
} }
} }
} }

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

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

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

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

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

@ -32,7 +32,7 @@
public override Rect GetBounds() public override Rect GetBounds()
{ {
return GlyphRun != null ? new Rect(GlyphRun.Size) : Rect.Empty; return GlyphRun != null ? new Rect(GlyphRun.Size) : default;
} }
} }
} }

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

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

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

@ -12,7 +12,7 @@ namespace Avalonia.Media
/// <summary> /// <summary>
/// Gets or sets the length of alternating dashes and gaps. /// Gets or sets the length of alternating dashes and gaps.
/// </summary> /// </summary>
IReadOnlyList<double> Dashes { get; } IReadOnlyList<double>? Dashes { get; }
/// <summary> /// <summary>
/// Gets or sets how far in the dash sequence the stroke will start. /// Gets or sets how far in the dash sequence the stroke will start.

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

@ -42,7 +42,7 @@ namespace Avalonia.Media
var imageSource = ImageSource; var imageSource = ImageSource;
var rect = Rect; var rect = Rect;
if (imageSource is object && !rect.IsEmpty) if (imageSource is object && !rect.IsDefault)
{ {
context.DrawImage(imageSource, rect); context.DrawImage(imageSource, rect);
} }

4
src/Avalonia.Base/Media/Imaging/CroppedBitmap.cs

@ -78,8 +78,8 @@ namespace Avalonia.Media.Imaging
get get
{ {
if (Source is not IBitmap bmp) if (Source is not IBitmap bmp)
return Size.Empty; return default;
if (SourceRect.IsEmpty) if (SourceRect.IsDefault)
return Source.Size; return Source.Size;
return SourceRect.Size.ToSizeWithDpi(bmp.Dpi); return SourceRect.Size.ToSizeWithDpi(bmp.Dpi);
} }

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

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

4
src/Avalonia.Base/Media/Immutable/ImmutableDashStyle.cs

@ -17,7 +17,7 @@ namespace Avalonia.Media.Immutable
/// </summary> /// </summary>
/// <param name="dashes">The dashes collection.</param> /// <param name="dashes">The dashes collection.</param>
/// <param name="offset">The dash sequence offset.</param> /// <param name="offset">The dash sequence offset.</param>
public ImmutableDashStyle(IEnumerable<double> dashes, double offset) public ImmutableDashStyle(IEnumerable<double>? dashes, double offset)
{ {
_dashes = dashes?.ToArray() ?? Array.Empty<double>(); _dashes = dashes?.ToArray() ?? Array.Empty<double>();
Offset = offset; Offset = offset;
@ -69,7 +69,7 @@ namespace Avalonia.Media.Immutable
return hashCode; return hashCode;
} }
private static bool SequenceEqual(IReadOnlyList<double> left, IReadOnlyList<double> right) private static bool SequenceEqual(IReadOnlyList<double> left, IReadOnlyList<double>? right)
{ {
if (ReferenceEquals(left, right)) if (ReferenceEquals(left, right))
{ {

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

5
src/Avalonia.Base/Media/TextFormatting/TextLeadingPrefixCharacterEllipsis.cs

@ -1,6 +1,5 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using Avalonia.Utilities;
namespace Avalonia.Media.TextFormatting namespace Avalonia.Media.TextFormatting
{ {
@ -19,7 +18,7 @@ namespace Avalonia.Media.TextFormatting
/// <param name="width">width in which collapsing is constrained to</param> /// <param name="width">width in which collapsing is constrained to</param>
/// <param name="textRunProperties">text run properties of ellipsis symbol</param> /// <param name="textRunProperties">text run properties of ellipsis symbol</param>
public TextLeadingPrefixCharacterEllipsis( public TextLeadingPrefixCharacterEllipsis(
ReadOnlySlice<char> ellipsis, string ellipsis,
int prefixLength, int prefixLength,
double width, double width,
TextRunProperties textRunProperties) TextRunProperties textRunProperties)
@ -129,7 +128,7 @@ namespace Avalonia.Media.TextFormatting
if (suffixCount > 0) if (suffixCount > 0)
{ {
var splitSuffix = var splitSuffix =
endShapedRun.Split(run.TextSourceLength - suffixCount); endShapedRun.Split(run.Length - suffixCount);
collapsedRuns.Add(splitSuffix.Second!); collapsedRuns.Add(splitSuffix.Second!);
} }

362
src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs

@ -56,7 +56,7 @@ namespace Avalonia.Media.TextFormatting
public override double Height => _textLineMetrics.Height; public override double Height => _textLineMetrics.Height;
/// <inheritdoc/> /// <inheritdoc/>
public override int NewLineLength => _textLineMetrics.NewLineLength; public override int NewLineLength => _textLineMetrics.NewlineLength;
/// <inheritdoc/> /// <inheritdoc/>
public override double OverhangAfter => 0; public override double OverhangAfter => 0;
@ -180,7 +180,7 @@ namespace Avalonia.Media.TextFormatting
{ {
var lastRun = _textRuns[_textRuns.Count - 1]; var lastRun = _textRuns[_textRuns.Count - 1];
return GetRunCharacterHit(lastRun, FirstTextSourceIndex + Length - lastRun.TextSourceLength, lastRun.Size.Width); return GetRunCharacterHit(lastRun, FirstTextSourceIndex + Length - lastRun.Length, lastRun.Size.Width);
} }
// process hit that happens within the line // process hit that happens within the line
@ -195,18 +195,18 @@ namespace Avalonia.Media.TextFormatting
if (currentRun is ShapedTextCharacters shapedRun && !shapedRun.ShapedBuffer.IsLeftToRight) if (currentRun is ShapedTextCharacters shapedRun && !shapedRun.ShapedBuffer.IsLeftToRight)
{ {
var rightToLeftIndex = i; var rightToLeftIndex = i;
currentPosition += currentRun.TextSourceLength; currentPosition += currentRun.Length;
while (rightToLeftIndex + 1 <= _textRuns.Count - 1) while (rightToLeftIndex + 1 <= _textRuns.Count - 1)
{ {
var nextShaped = _textRuns[rightToLeftIndex + 1] as ShapedTextCharacters; var nextShaped = _textRuns[++rightToLeftIndex] as ShapedTextCharacters;
if (nextShaped == null || nextShaped.ShapedBuffer.IsLeftToRight) if (nextShaped == null || nextShaped.ShapedBuffer.IsLeftToRight)
{ {
break; break;
} }
currentPosition += nextShaped.TextSourceLength; currentPosition += nextShaped.Length;
rightToLeftIndex++; rightToLeftIndex++;
} }
@ -223,27 +223,26 @@ namespace Avalonia.Media.TextFormatting
if (currentDistance + currentRun.Size.Width <= distance) if (currentDistance + currentRun.Size.Width <= distance)
{ {
currentDistance += currentRun.Size.Width; currentDistance += currentRun.Size.Width;
currentPosition -= currentRun.TextSourceLength; currentPosition -= currentRun.Length;
continue; continue;
} }
characterHit = GetRunCharacterHit(currentRun, currentPosition, distance - currentDistance); return GetRunCharacterHit(currentRun, currentPosition, distance - currentDistance);
break;
} }
} }
if (currentDistance + currentRun.Size.Width < distance) characterHit = GetRunCharacterHit(currentRun, currentPosition, distance - currentDistance);
if (i < _textRuns.Count - 1 && currentDistance + currentRun.Size.Width < distance)
{ {
currentDistance += currentRun.Size.Width; currentDistance += currentRun.Size.Width;
currentPosition += currentRun.TextSourceLength;
currentPosition += currentRun.Length;
continue; continue;
} }
characterHit = GetRunCharacterHit(currentRun, currentPosition, distance - currentDistance);
break; break;
} }
@ -264,10 +263,10 @@ namespace Avalonia.Media.TextFormatting
if (shapedRun.GlyphRun.IsLeftToRight) if (shapedRun.GlyphRun.IsLeftToRight)
{ {
offset = Math.Max(0, currentPosition - shapedRun.Text.Start); offset = Math.Max(0, currentPosition - shapedRun.GlyphRun.Metrics.FirstCluster);
} }
characterHit = new CharacterHit(characterHit.FirstCharacterIndex + offset, characterHit.TrailingLength); characterHit = new CharacterHit(offset + characterHit.FirstCharacterIndex, characterHit.TrailingLength);
break; break;
} }
@ -279,7 +278,7 @@ namespace Avalonia.Media.TextFormatting
} }
else else
{ {
characterHit = new CharacterHit(currentPosition, run.TextSourceLength); characterHit = new CharacterHit(currentPosition, run.Length);
} }
break; break;
} }
@ -334,14 +333,14 @@ namespace Avalonia.Media.TextFormatting
rightToLeftWidth -= currentRun.Size.Width; rightToLeftWidth -= currentRun.Size.Width;
if (currentPosition + currentRun.TextSourceLength >= characterIndex) if (currentPosition + currentRun.Length >= characterIndex)
{ {
break; break;
} }
currentPosition += currentRun.TextSourceLength; currentPosition += currentRun.Length;
remainingLength -= currentRun.TextSourceLength; remainingLength -= currentRun.Length;
i--; i--;
} }
@ -350,7 +349,7 @@ namespace Avalonia.Media.TextFormatting
} }
} }
if (currentPosition + currentRun.TextSourceLength >= characterIndex && if (currentPosition + currentRun.Length >= characterIndex &&
TryGetDistanceFromCharacterHit(currentRun, characterHit, currentPosition, remainingLength, flowDirection, out var distance, out _)) TryGetDistanceFromCharacterHit(currentRun, characterHit, currentPosition, remainingLength, flowDirection, out var distance, out _))
{ {
return Math.Max(0, currentDistance + distance); return Math.Max(0, currentDistance + distance);
@ -358,8 +357,8 @@ namespace Avalonia.Media.TextFormatting
//No hit hit found so we add the full width //No hit hit found so we add the full width
currentDistance += currentRun.Size.Width; currentDistance += currentRun.Size.Width;
currentPosition += currentRun.TextSourceLength; currentPosition += currentRun.Length;
remainingLength -= currentRun.TextSourceLength; remainingLength -= currentRun.Length;
} }
} }
else else
@ -383,8 +382,8 @@ namespace Avalonia.Media.TextFormatting
//No hit hit found so we add the full width //No hit hit found so we add the full width
currentDistance -= currentRun.Size.Width; currentDistance -= currentRun.Size.Width;
currentPosition += currentRun.TextSourceLength; currentPosition += currentRun.Length;
remainingLength -= currentRun.TextSourceLength; remainingLength -= currentRun.Length;
} }
} }
@ -412,16 +411,16 @@ namespace Avalonia.Media.TextFormatting
{ {
currentGlyphRun = shapedTextCharacters.GlyphRun; currentGlyphRun = shapedTextCharacters.GlyphRun;
if (currentPosition + remainingLength <= currentPosition + currentRun.Text.Length) if (currentPosition + remainingLength <= currentPosition + currentRun.Length)
{ {
characterHit = new CharacterHit(currentRun.Text.Start + remainingLength); characterHit = new CharacterHit(currentPosition + remainingLength);
distance = currentGlyphRun.GetDistanceFromCharacterHit(characterHit); distance = currentGlyphRun.GetDistanceFromCharacterHit(characterHit);
return true; return true;
} }
if (currentPosition + remainingLength == currentPosition + currentRun.Text.Length && isTrailingHit) if (currentPosition + remainingLength == currentPosition + currentRun.Length && isTrailingHit)
{ {
if (currentGlyphRun.IsLeftToRight || flowDirection == FlowDirection.RightToLeft) if (currentGlyphRun.IsLeftToRight || flowDirection == FlowDirection.RightToLeft)
{ {
@ -440,7 +439,7 @@ namespace Avalonia.Media.TextFormatting
return true; return true;
} }
if (characterIndex == currentPosition + currentRun.TextSourceLength) if (characterIndex == currentPosition + currentRun.Length)
{ {
distance = currentRun.Size.Width; distance = currentRun.Size.Width;
@ -479,17 +478,22 @@ namespace Avalonia.Media.TextFormatting
{ {
case ShapedTextCharacters shapedRun: case ShapedTextCharacters shapedRun:
{ {
characterHit = shapedRun.GlyphRun.GetNextCaretCharacterHit(characterHit); nextCharacterHit = shapedRun.GlyphRun.GetNextCaretCharacterHit(characterHit);
break; break;
} }
default: default:
{ {
characterHit = new CharacterHit(currentPosition + currentRun.TextSourceLength); nextCharacterHit = new CharacterHit(currentPosition + currentRun.Length);
break; break;
} }
} }
return characterHit; if (characterHit.FirstCharacterIndex + characterHit.TrailingLength == nextCharacterHit.FirstCharacterIndex + nextCharacterHit.TrailingLength)
{
return characterHit;
}
return nextCharacterHit;
} }
/// <inheritdoc/> /// <inheritdoc/>
@ -528,7 +532,7 @@ namespace Avalonia.Media.TextFormatting
var startX = Start; var startX = Start;
double currentWidth = 0; double currentWidth = 0;
var currentRect = Rect.Empty; var currentRect = default(Rect);
TextRunBounds lastRunBounds = default; TextRunBounds lastRunBounds = default;
@ -542,200 +546,182 @@ namespace Avalonia.Media.TextFormatting
var characterLength = 0; var characterLength = 0;
var endX = startX; var endX = startX;
var currentShapedRun = currentRun as ShapedTextCharacters;
TextRunBounds currentRunBounds; TextRunBounds currentRunBounds;
double combinedWidth; double combinedWidth;
if (currentPosition + currentRun.TextSourceLength <= firstTextSourceIndex) if (currentRun is ShapedTextCharacters currentShapedRun)
{
startX += currentRun.Size.Width;
currentPosition += currentRun.TextSourceLength;
continue;
}
if (currentShapedRun != null && !currentShapedRun.ShapedBuffer.IsLeftToRight)
{ {
var rightToLeftIndex = index; var firstCluster = currentShapedRun.GlyphRun.Metrics.FirstCluster;
var rightToLeftWidth = currentShapedRun.Size.Width;
while (rightToLeftIndex + 1 <= _textRuns.Count - 1 && _textRuns[rightToLeftIndex + 1] is ShapedTextCharacters nextShapedRun) if (currentPosition + currentRun.Length <= firstTextSourceIndex)
{ {
if (nextShapedRun == null || nextShapedRun.ShapedBuffer.IsLeftToRight) startX += currentRun.Size.Width;
{
break;
}
rightToLeftIndex++; currentPosition += currentRun.Length;
rightToLeftWidth += nextShapedRun.Size.Width; continue;
if (currentPosition + nextShapedRun.TextSourceLength > firstTextSourceIndex + textLength)
{
break;
}
currentShapedRun = nextShapedRun;
} }
startX = startX + rightToLeftWidth; if (currentShapedRun.ShapedBuffer.IsLeftToRight)
{
var startIndex = firstCluster + Math.Max(0, firstTextSourceIndex - currentPosition);
currentRunBounds = GetRightToLeftTextRunBounds(currentShapedRun, startX, firstTextSourceIndex, characterIndex, currentPosition, remainingLength); double startOffset;
remainingLength -= currentRunBounds.Length; double endOffset;
currentPosition = currentRunBounds.TextSourceCharacterIndex + currentRunBounds.Length;
endX = currentRunBounds.Rectangle.Right;
startX = currentRunBounds.Rectangle.Left;
var rightToLeftRunBounds = new List<TextRunBounds> { currentRunBounds }; startOffset = currentShapedRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex));
for (int i = rightToLeftIndex - 1; i >= index; i--) endOffset = currentShapedRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex + remainingLength));
{
currentShapedRun = TextRuns[i] as ShapedTextCharacters;
if(currentShapedRun == null) startX += startOffset;
{
continue;
}
currentRunBounds = GetRightToLeftTextRunBounds(currentShapedRun, startX, firstTextSourceIndex, characterIndex, currentPosition, remainingLength); endX += endOffset;
rightToLeftRunBounds.Insert(0, currentRunBounds); var endHit = currentShapedRun.GlyphRun.GetCharacterHitFromDistance(endOffset, out _);
remainingLength -= currentRunBounds.Length; var startHit = currentShapedRun.GlyphRun.GetCharacterHitFromDistance(startOffset, out _);
startX = currentRunBounds.Rectangle.Left;
currentPosition += currentRunBounds.Length; characterLength = Math.Abs(endHit.FirstCharacterIndex + endHit.TrailingLength - startHit.FirstCharacterIndex - startHit.TrailingLength);
currentDirection = FlowDirection.LeftToRight;
} }
else
{
var rightToLeftIndex = index;
var rightToLeftWidth = currentShapedRun.Size.Width;
combinedWidth = endX - startX; while (rightToLeftIndex + 1 <= _textRuns.Count - 1 && _textRuns[rightToLeftIndex + 1] is ShapedTextCharacters nextShapedRun)
{
if (nextShapedRun == null || nextShapedRun.ShapedBuffer.IsLeftToRight)
{
break;
}
currentRect = new Rect(startX, 0, combinedWidth, Height); rightToLeftIndex++;
currentDirection = FlowDirection.RightToLeft; rightToLeftWidth += nextShapedRun.Size.Width;
if (!MathUtilities.IsZero(combinedWidth)) if (currentPosition + nextShapedRun.Length > firstTextSourceIndex + textLength)
{ {
result.Add(new TextBounds(currentRect, currentDirection, rightToLeftRunBounds)); break;
} }
startX = endX; currentShapedRun = nextShapedRun;
} }
else
{
if (currentShapedRun != null)
{
var offset = Math.Max(0, firstTextSourceIndex - currentPosition);
currentPosition += offset; startX += rightToLeftWidth;
var startIndex = currentRun.Text.Start + offset; currentRunBounds = GetRightToLeftTextRunBounds(currentShapedRun, startX, firstTextSourceIndex, characterIndex, currentPosition, remainingLength);
double startOffset; remainingLength -= currentRunBounds.Length;
double endOffset; currentPosition = currentRunBounds.TextSourceCharacterIndex + currentRunBounds.Length;
endX = currentRunBounds.Rectangle.Right;
startX = currentRunBounds.Rectangle.Left;
if (currentShapedRun.ShapedBuffer.IsLeftToRight) var rightToLeftRunBounds = new List<TextRunBounds> { currentRunBounds };
{
startOffset = currentShapedRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex));
endOffset = currentShapedRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex + remainingLength)); for (int i = rightToLeftIndex - 1; i >= index; i--)
}
else
{ {
endOffset = currentShapedRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex)); if (TextRuns[i] is not ShapedTextCharacters)
if (currentPosition < startIndex)
{
startOffset = endOffset;
}
else
{ {
startOffset = currentShapedRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex + remainingLength)); continue;
} }
}
startX += startOffset; currentShapedRun = (ShapedTextCharacters)TextRuns[i];
endX += endOffset; currentRunBounds = GetRightToLeftTextRunBounds(currentShapedRun, startX, firstTextSourceIndex, characterIndex, currentPosition, remainingLength);
var endHit = currentShapedRun.GlyphRun.GetCharacterHitFromDistance(endOffset, out _); rightToLeftRunBounds.Insert(0, currentRunBounds);
var startHit = currentShapedRun.GlyphRun.GetCharacterHitFromDistance(startOffset, out _);
characterLength = Math.Abs(endHit.FirstCharacterIndex + endHit.TrailingLength - startHit.FirstCharacterIndex - startHit.TrailingLength); remainingLength -= currentRunBounds.Length;
startX = currentRunBounds.Rectangle.Left;
currentDirection = FlowDirection.LeftToRight; currentPosition += currentRunBounds.Length;
} }
else
{
if (currentPosition + currentRun.TextSourceLength <= firstTextSourceIndex)
{
startX += currentRun.Size.Width;
currentPosition += currentRun.TextSourceLength; combinedWidth = endX - startX;
continue; currentRect = new Rect(startX, 0, combinedWidth, Height);
}
currentDirection = FlowDirection.RightToLeft;
if (currentPosition < firstTextSourceIndex) if (!MathUtilities.IsZero(combinedWidth))
{ {
startX += currentRun.Size.Width; result.Add(new TextBounds(currentRect, currentDirection, rightToLeftRunBounds));
} }
if (currentPosition + currentRun.TextSourceLength <= characterIndex) startX = endX;
{ }
endX += currentRun.Size.Width; }
else
{
if (currentPosition + currentRun.Length <= firstTextSourceIndex)
{
startX += currentRun.Size.Width;
characterLength = currentRun.TextSourceLength; currentPosition += currentRun.Length;
}
continue;
} }
if (endX < startX) if (currentPosition < firstTextSourceIndex)
{ {
(endX, startX) = (startX, endX); startX += currentRun.Size.Width;
} }
//Lines that only contain a linebreak need to be covered here if (currentPosition + currentRun.Length <= characterIndex)
if (characterLength == 0)
{ {
characterLength = NewLineLength; endX += currentRun.Size.Width;
characterLength = currentRun.Length;
} }
}
combinedWidth = endX - startX; if (endX < startX)
{
(endX, startX) = (startX, endX);
}
currentRunBounds = new TextRunBounds(new Rect(startX, 0, combinedWidth, Height), currentPosition, characterLength, currentRun); //Lines that only contain a linebreak need to be covered here
if (characterLength == 0)
{
characterLength = NewLineLength;
}
currentPosition += characterLength; combinedWidth = endX - startX;
remainingLength -= characterLength; currentRunBounds = new TextRunBounds(new Rect(startX, 0, combinedWidth, Height), currentPosition, characterLength, currentRun);
startX = endX; currentPosition += characterLength;
if (currentRunBounds.TextRun != null && !MathUtilities.IsZero(combinedWidth) || NewLineLength > 0) remainingLength -= characterLength;
{
if (result.Count > 0 && lastDirection == currentDirection && MathUtilities.AreClose(currentRect.Left, lastRunBounds.Rectangle.Right))
{
currentRect = currentRect.WithWidth(currentWidth + combinedWidth);
var textBounds = result[result.Count - 1]; startX = endX;
textBounds.Rectangle = currentRect; if (currentRunBounds.TextRun != null && !MathUtilities.IsZero(combinedWidth) || NewLineLength > 0)
{
if (result.Count > 0 && lastDirection == currentDirection && MathUtilities.AreClose(currentRect.Left, lastRunBounds.Rectangle.Right))
{
currentRect = currentRect.WithWidth(currentWidth + combinedWidth);
textBounds.TextRunBounds.Add(currentRunBounds); var textBounds = result[result.Count - 1];
}
else
{
currentRect = currentRunBounds.Rectangle;
result.Add(new TextBounds(currentRect, currentDirection, new List<TextRunBounds> { currentRunBounds })); textBounds.Rectangle = currentRect;
}
textBounds.TextRunBounds.Add(currentRunBounds);
} }
else
{
currentRect = currentRunBounds.Rectangle;
lastRunBounds = currentRunBounds; result.Add(new TextBounds(currentRect, currentDirection, new List<TextRunBounds> { currentRunBounds }));
}
} }
lastRunBounds = currentRunBounds;
currentWidth += combinedWidth; currentWidth += combinedWidth;
if (remainingLength <= 0 || currentPosition >= characterIndex) if (remainingLength <= 0 || currentPosition >= characterIndex)
@ -762,7 +748,7 @@ namespace Avalonia.Media.TextFormatting
var startX = WidthIncludingTrailingWhitespace; var startX = WidthIncludingTrailingWhitespace;
double currentWidth = 0; double currentWidth = 0;
var currentRect = Rect.Empty; var currentRect = default(Rect);
for (var index = TextRuns.Count - 1; index >= 0; index--) for (var index = TextRuns.Count - 1; index >= 0; index--)
{ {
@ -771,11 +757,11 @@ namespace Avalonia.Media.TextFormatting
continue; continue;
} }
if (currentPosition + currentRun.TextSourceLength < firstTextSourceIndex) if (currentPosition + currentRun.Length < firstTextSourceIndex)
{ {
startX -= currentRun.Size.Width; startX -= currentRun.Size.Width;
currentPosition += currentRun.TextSourceLength; currentPosition += currentRun.Length;
continue; continue;
} }
@ -789,7 +775,7 @@ namespace Avalonia.Media.TextFormatting
currentPosition += offset; currentPosition += offset;
var startIndex = currentRun.Text.Start + offset; var startIndex = currentPosition;
double startOffset; double startOffset;
double endOffset; double endOffset;
@ -827,7 +813,7 @@ namespace Avalonia.Media.TextFormatting
} }
else else
{ {
if (currentPosition + currentRun.TextSourceLength <= characterIndex) if (currentPosition + currentRun.Length <= characterIndex)
{ {
endX -= currentRun.Size.Width; endX -= currentRun.Size.Width;
} }
@ -836,7 +822,7 @@ namespace Avalonia.Media.TextFormatting
{ {
startX -= currentRun.Size.Width; startX -= currentRun.Size.Width;
characterLength = currentRun.TextSourceLength; characterLength = currentRun.Length;
} }
} }
@ -905,7 +891,7 @@ namespace Avalonia.Media.TextFormatting
currentPosition += offset; currentPosition += offset;
var startIndex = currentRun.Text.Start + offset; var startIndex = currentPosition;
double startOffset; double startOffset;
double endOffset; double endOffset;
@ -1172,12 +1158,12 @@ namespace Avalonia.Media.TextFormatting
return true; return true;
} }
var characterIndex = codepointIndex - shapedRun.Text.Start; //var characterIndex = codepointIndex - shapedRun.Text.Start;
if (characterIndex < 0 && shapedRun.ShapedBuffer.IsLeftToRight) //if (characterIndex < 0 && shapedRun.ShapedBuffer.IsLeftToRight)
{ //{
foundCharacterHit = new CharacterHit(foundCharacterHit.FirstCharacterIndex); // foundCharacterHit = new CharacterHit(foundCharacterHit.FirstCharacterIndex);
} //}
nextCharacterHit = isAtEnd || characterHit.TrailingLength != 0 ? nextCharacterHit = isAtEnd || characterHit.TrailingLength != 0 ?
foundCharacterHit : foundCharacterHit :
@ -1196,7 +1182,7 @@ namespace Avalonia.Media.TextFormatting
if (textPosition == currentPosition) if (textPosition == currentPosition)
{ {
nextCharacterHit = new CharacterHit(currentPosition + currentRun.TextSourceLength); nextCharacterHit = new CharacterHit(currentPosition + currentRun.Length);
return true; return true;
} }
@ -1205,7 +1191,7 @@ namespace Avalonia.Media.TextFormatting
} }
} }
currentPosition += currentRun.TextSourceLength; currentPosition += currentRun.Length;
runIndex++; runIndex++;
} }
@ -1271,7 +1257,7 @@ namespace Avalonia.Media.TextFormatting
} }
default: default:
{ {
if (characterIndex == currentPosition + currentRun.TextSourceLength) if (characterIndex == currentPosition + currentRun.Length)
{ {
previousCharacterHit = new CharacterHit(currentPosition); previousCharacterHit = new CharacterHit(currentPosition);
@ -1282,7 +1268,7 @@ namespace Avalonia.Media.TextFormatting
} }
} }
currentPosition -= currentRun.TextSourceLength; currentPosition -= currentRun.Length;
runIndex--; runIndex--;
} }
@ -1310,18 +1296,25 @@ namespace Avalonia.Media.TextFormatting
{ {
case ShapedTextCharacters shapedRun: case ShapedTextCharacters shapedRun:
{ {
var firstCluster = shapedRun.GlyphRun.Metrics.FirstCluster;
if (firstCluster > codepointIndex)
{
break;
}
if (previousRun is ShapedTextCharacters previousShaped && !previousShaped.ShapedBuffer.IsLeftToRight) if (previousRun is ShapedTextCharacters previousShaped && !previousShaped.ShapedBuffer.IsLeftToRight)
{ {
if (shapedRun.ShapedBuffer.IsLeftToRight) if (shapedRun.ShapedBuffer.IsLeftToRight)
{ {
if (currentRun.Text.Start >= codepointIndex) if (firstCluster >= codepointIndex)
{ {
return --runIndex; return --runIndex;
} }
} }
else else
{ {
if (codepointIndex > currentRun.Text.Start + currentRun.Text.Length) if (codepointIndex > firstCluster + currentRun.Length)
{ {
return --runIndex; return --runIndex;
} }
@ -1330,15 +1323,15 @@ namespace Avalonia.Media.TextFormatting
if (direction == LogicalDirection.Forward) if (direction == LogicalDirection.Forward)
{ {
if (codepointIndex >= currentRun.Text.Start && codepointIndex <= currentRun.Text.End) if (codepointIndex >= firstCluster && codepointIndex <= firstCluster + currentRun.Length)
{ {
return runIndex; return runIndex;
} }
} }
else else
{ {
if (codepointIndex > currentRun.Text.Start && if (codepointIndex > firstCluster &&
codepointIndex <= currentRun.Text.Start + currentRun.Text.Length) codepointIndex <= firstCluster + currentRun.Length)
{ {
return runIndex; return runIndex;
} }
@ -1349,6 +1342,8 @@ namespace Avalonia.Media.TextFormatting
return runIndex; return runIndex;
} }
textPosition += currentRun.Length;
break; break;
} }
@ -1364,13 +1359,14 @@ namespace Avalonia.Media.TextFormatting
return runIndex; return runIndex;
} }
textPosition += currentRun.Length;
break; break;
} }
} }
runIndex++; runIndex++;
previousRun = currentRun; previousRun = currentRun;
textPosition += currentRun.TextSourceLength;
} }
return runIndex; return runIndex;
@ -1401,7 +1397,7 @@ namespace Avalonia.Media.TextFormatting
case ShapedTextCharacters textRun: case ShapedTextCharacters textRun:
{ {
var textMetrics = var textMetrics =
new TextMetrics(textRun.Properties.Typeface, textRun.Properties.FontRenderingEmSize); new TextMetrics(textRun.Properties.Typeface.GlyphTypeface, textRun.Properties.FontRenderingEmSize);
if (fontRenderingEmSize < textRun.Properties.FontRenderingEmSize) if (fontRenderingEmSize < textRun.Properties.FontRenderingEmSize)
{ {
@ -1432,7 +1428,7 @@ namespace Avalonia.Media.TextFormatting
{ {
width = widthIncludingWhitespace + textRun.GlyphRun.Metrics.Width; width = widthIncludingWhitespace + textRun.GlyphRun.Metrics.Width;
trailingWhitespaceLength = textRun.GlyphRun.Metrics.TrailingWhitespaceLength; trailingWhitespaceLength = textRun.GlyphRun.Metrics.TrailingWhitespaceLength;
newLineLength = textRun.GlyphRun.Metrics.NewlineLength; newLineLength = textRun.GlyphRun.Metrics.NewLineLength;
} }
widthIncludingWhitespace += textRun.GlyphRun.Metrics.WidthIncludingTrailingWhitespace; widthIncludingWhitespace += textRun.GlyphRun.Metrics.WidthIncludingTrailingWhitespace;

8
src/Avalonia.Base/Media/TextFormatting/TextLineMetrics.cs

@ -4,15 +4,15 @@
/// Represents a metric for a <see cref="TextLine"/> objects, /// Represents a metric for a <see cref="TextLine"/> objects,
/// that holds information about ascent, descent, line gap, size and origin of the text line. /// that holds information about ascent, descent, line gap, size and origin of the text line.
/// </summary> /// </summary>
public readonly struct TextLineMetrics public readonly record struct TextLineMetrics
{ {
public TextLineMetrics(bool hasOverflowed, double height, int newLineLength, double start, double textBaseline, public TextLineMetrics(bool hasOverflowed, double height, int newlineLength, double start, double textBaseline,
int trailingWhitespaceLength, double width, int trailingWhitespaceLength, double width,
double widthIncludingTrailingWhitespace) double widthIncludingTrailingWhitespace)
{ {
HasOverflowed = hasOverflowed; HasOverflowed = hasOverflowed;
Height = height; Height = height;
NewLineLength = newLineLength; NewlineLength = newlineLength;
Start = start; Start = start;
TextBaseline = textBaseline; TextBaseline = textBaseline;
TrailingWhitespaceLength = trailingWhitespaceLength; TrailingWhitespaceLength = trailingWhitespaceLength;
@ -33,7 +33,7 @@
/// <summary> /// <summary>
/// Gets the number of newline characters at the end of a line. /// Gets the number of newline characters at the end of a line.
/// </summary> /// </summary>
public int NewLineLength { get; } public int NewlineLength { get; }
/// <summary> /// <summary>
/// Gets the distance from the start of a paragraph to the starting point of a line. /// Gets the distance from the start of a paragraph to the starting point of a line.

6
src/Avalonia.Base/Media/TextFormatting/TextMetrics.cs

@ -3,11 +3,11 @@
/// <summary> /// <summary>
/// A metric that holds information about text specific measurements. /// A metric that holds information about text specific measurements.
/// </summary> /// </summary>
public readonly struct TextMetrics public readonly record struct TextMetrics
{ {
public TextMetrics(Typeface typeface, double fontRenderingEmSize) public TextMetrics(IGlyphTypeface glyphTypeface, double fontRenderingEmSize)
{ {
var fontMetrics = typeface.GlyphTypeface.Metrics; var fontMetrics = glyphTypeface.Metrics;
var scale = fontRenderingEmSize / fontMetrics.DesignEmHeight; var scale = fontRenderingEmSize / fontMetrics.DesignEmHeight;

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

@ -5,7 +5,7 @@ namespace Avalonia.Media.TextFormatting
/// <summary> /// <summary>
/// References a portion of a text buffer. /// References a portion of a text buffer.
/// </summary> /// </summary>
public readonly struct TextRange public readonly record struct TextRange
{ {
public TextRange(int start, int length) public TextRange(int start, int length)
{ {

11
src/Avalonia.Base/Media/TextFormatting/TextRun.cs

@ -1,5 +1,4 @@
using System.Diagnostics; using System.Diagnostics;
using Avalonia.Utilities;
namespace Avalonia.Media.TextFormatting namespace Avalonia.Media.TextFormatting
{ {
@ -14,12 +13,12 @@ namespace Avalonia.Media.TextFormatting
/// <summary> /// <summary>
/// Gets the text source length. /// Gets the text source length.
/// </summary> /// </summary>
public virtual int TextSourceLength => DefaultTextSourceLength; public virtual int Length => DefaultTextSourceLength;
/// <summary> /// <summary>
/// Gets the text run's text. /// Gets the text run's text.
/// </summary> /// </summary>
public virtual ReadOnlySlice<char> Text => default; public virtual CharacterBufferReference CharacterBufferReference => default;
/// <summary> /// <summary>
/// A set of properties shared by every characters in the run /// A set of properties shared by every characters in the run
@ -41,9 +40,11 @@ namespace Avalonia.Media.TextFormatting
{ {
unsafe unsafe
{ {
fixed (char* charsPtr = _textRun.Text.Span) var characterBuffer = _textRun.CharacterBufferReference.CharacterBuffer;
fixed (char* charsPtr = characterBuffer.Span)
{ {
return new string(charsPtr, 0, _textRun.Text.Length); return new string(charsPtr, 0, characterBuffer.Span.Length);
} }
} }
} }

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

@ -3,7 +3,7 @@
/// <summary> /// <summary>
/// The bounding rectangle of text run /// The bounding rectangle of text run
/// </summary> /// </summary>
public readonly struct TextRunBounds public readonly record struct TextRunBounds
{ {
/// <summary> /// <summary>
/// Constructing TextRunBounds /// Constructing TextRunBounds

11
src/Avalonia.Base/Media/TextFormatting/TextShaper.cs

@ -1,7 +1,5 @@
using System; using System;
using System.Globalization;
using Avalonia.Platform; using Avalonia.Platform;
using Avalonia.Utilities;
namespace Avalonia.Media.TextFormatting namespace Avalonia.Media.TextFormatting
{ {
@ -45,9 +43,14 @@ namespace Avalonia.Media.TextFormatting
} }
/// <inheritdoc cref="ITextShaperImpl.ShapeText"/> /// <inheritdoc cref="ITextShaperImpl.ShapeText"/>
public ShapedBuffer ShapeText(ReadOnlySlice<char> text, TextShaperOptions options) public ShapedBuffer ShapeText(CharacterBufferReference text, int length, TextShaperOptions options = default)
{ {
return _platformImpl.ShapeText(text, options); return _platformImpl.ShapeText(text, length, options);
}
public ShapedBuffer ShapeText(string text, TextShaperOptions options = default)
{
return ShapeText(new CharacterBufferReference(text), text.Length, options);
} }
} }
} }

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

@ -5,7 +5,7 @@ namespace Avalonia.Media.TextFormatting
/// <summary> /// <summary>
/// Options to customize text shaping. /// Options to customize text shaping.
/// </summary> /// </summary>
public readonly struct TextShaperOptions public readonly record struct TextShaperOptions
{ {
public TextShaperOptions( public TextShaperOptions(
IGlyphTypeface typeface, IGlyphTypeface typeface,

3
src/Avalonia.Base/Media/TextFormatting/TextTrailingCharacterEllipsis.cs

@ -1,5 +1,4 @@
using System.Collections.Generic; using System.Collections.Generic;
using Avalonia.Utilities;
namespace Avalonia.Media.TextFormatting namespace Avalonia.Media.TextFormatting
{ {
@ -15,7 +14,7 @@ namespace Avalonia.Media.TextFormatting
/// <param name="ellipsis">Text used as collapsing symbol.</param> /// <param name="ellipsis">Text used as collapsing symbol.</param>
/// <param name="width">Width in which collapsing is constrained to.</param> /// <param name="width">Width in which collapsing is constrained to.</param>
/// <param name="textRunProperties">Text run properties of ellipsis symbol.</param> /// <param name="textRunProperties">Text run properties of ellipsis symbol.</param>
public TextTrailingCharacterEllipsis(ReadOnlySlice<char> ellipsis, double width, TextRunProperties textRunProperties) public TextTrailingCharacterEllipsis(string ellipsis, double width, TextRunProperties textRunProperties)
{ {
Width = width; Width = width;
Symbol = new TextCharacters(ellipsis, textRunProperties); Symbol = new TextCharacters(ellipsis, textRunProperties);

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

@ -16,7 +16,7 @@ namespace Avalonia.Media.TextFormatting
/// <param name="width">width in which collapsing is constrained to.</param> /// <param name="width">width in which collapsing is constrained to.</param>
/// <param name="textRunProperties">text run properties of ellipsis symbol.</param> /// <param name="textRunProperties">text run properties of ellipsis symbol.</param>
public TextTrailingWordEllipsis( public TextTrailingWordEllipsis(
ReadOnlySlice<char> ellipsis, string ellipsis,
double width, double width,
TextRunProperties textRunProperties TextRunProperties textRunProperties
) )

3
src/Avalonia.Base/Media/TextFormatting/Unicode/BiDiData.cs

@ -2,6 +2,7 @@
// Licensed under the Apache License, Version 2.0. // Licensed under the Apache License, Version 2.0.
// Ported from: https://github.com/SixLabors/Fonts/ // Ported from: https://github.com/SixLabors/Fonts/
using System;
using Avalonia.Utilities; using Avalonia.Utilities;
namespace Avalonia.Media.TextFormatting.Unicode namespace Avalonia.Media.TextFormatting.Unicode
@ -63,7 +64,7 @@ namespace Avalonia.Media.TextFormatting.Unicode
/// Appends text to the bidi data. /// Appends text to the bidi data.
/// </summary> /// </summary>
/// <param name="text">The text to process.</param> /// <param name="text">The text to process.</param>
public void Append(ReadOnlySlice<char> text) public void Append(CharacterBufferRange text)
{ {
_classes.Add(text.Length); _classes.Add(text.Length);
_pairedBracketTypes.Add(text.Length); _pairedBracketTypes.Add(text.Length);

11
src/Avalonia.Base/Media/TextFormatting/Unicode/Codepoint.cs

@ -1,10 +1,9 @@
using System; using System.Collections.Generic;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using Avalonia.Utilities;
namespace Avalonia.Media.TextFormatting.Unicode namespace Avalonia.Media.TextFormatting.Unicode
{ {
public readonly struct Codepoint public readonly record struct Codepoint
{ {
private readonly uint _value; private readonly uint _value;
@ -166,11 +165,11 @@ namespace Avalonia.Media.TextFormatting.Unicode
/// <param name="index">The index to read at.</param> /// <param name="index">The index to read at.</param>
/// <param name="count">The count of character that were read.</param> /// <param name="count">The count of character that were read.</param>
/// <returns></returns> /// <returns></returns>
public static Codepoint ReadAt(ReadOnlySpan<char> text, int index, out int count) public static Codepoint ReadAt(IReadOnlyList<char> text, int index, out int count)
{ {
count = 1; count = 1;
if (index >= text.Length) if (index >= text.Count)
{ {
return ReplacementCodepoint; return ReplacementCodepoint;
} }
@ -184,7 +183,7 @@ namespace Avalonia.Media.TextFormatting.Unicode
{ {
hi = code; hi = code;
if (index + 1 == text.Length) if (index + 1 == text.Count)
{ {
return ReplacementCodepoint; return ReplacementCodepoint;
} }

6
src/Avalonia.Base/Media/TextFormatting/Unicode/CodepointEnumerator.cs

@ -1,12 +1,12 @@
using Avalonia.Utilities; using System;
namespace Avalonia.Media.TextFormatting.Unicode namespace Avalonia.Media.TextFormatting.Unicode
{ {
public ref struct CodepointEnumerator public ref struct CodepointEnumerator
{ {
private ReadOnlySlice<char> _text; private CharacterBufferRange _text;
public CodepointEnumerator(ReadOnlySlice<char> text) public CodepointEnumerator(CharacterBufferRange text)
{ {
_text = text; _text = text;
Current = Codepoint.ReplacementCodepoint; Current = Codepoint.ReplacementCodepoint;

8
src/Avalonia.Base/Media/TextFormatting/Unicode/Grapheme.cs

@ -1,13 +1,13 @@
using Avalonia.Utilities; using System;
namespace Avalonia.Media.TextFormatting.Unicode namespace Avalonia.Media.TextFormatting.Unicode
{ {
/// <summary> /// <summary>
/// Represents the smallest unit of a writing system of any given language. /// Represents the smallest unit of a writing system of any given language.
/// </summary> /// </summary>
public readonly struct Grapheme public readonly ref struct Grapheme
{ {
public Grapheme(Codepoint firstCodepoint, ReadOnlySlice<char> text) public Grapheme(Codepoint firstCodepoint, ReadOnlySpan<char> text)
{ {
FirstCodepoint = firstCodepoint; FirstCodepoint = firstCodepoint;
Text = text; Text = text;
@ -21,6 +21,6 @@ namespace Avalonia.Media.TextFormatting.Unicode
/// <summary> /// <summary>
/// The text that is representing the <see cref="Grapheme"/>. /// The text that is representing the <see cref="Grapheme"/>.
/// </summary> /// </summary>
public ReadOnlySlice<char> Text { get; } public ReadOnlySpan<char> Text { get; }
} }
} }

12
src/Avalonia.Base/Media/TextFormatting/Unicode/GraphemeEnumerator.cs

@ -3,16 +3,16 @@
// //
// Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation. // Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation.
using System.Collections.Generic;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using Avalonia.Utilities;
namespace Avalonia.Media.TextFormatting.Unicode namespace Avalonia.Media.TextFormatting.Unicode
{ {
public ref struct GraphemeEnumerator public ref struct GraphemeEnumerator
{ {
private ReadOnlySlice<char> _text; private CharacterBufferRange _text;
public GraphemeEnumerator(ReadOnlySlice<char> text) public GraphemeEnumerator(CharacterBufferRange text)
{ {
_text = text; _text = text;
Current = default; Current = default;
@ -187,7 +187,7 @@ namespace Avalonia.Media.TextFormatting.Unicode
var text = _text.Take(processor.CurrentCodeUnitOffset); var text = _text.Take(processor.CurrentCodeUnitOffset);
Current = new Grapheme(firstCodepoint, text); Current = new Grapheme(firstCodepoint, text.Span);
_text = _text.Skip(processor.CurrentCodeUnitOffset); _text = _text.Skip(processor.CurrentCodeUnitOffset);
@ -197,10 +197,10 @@ namespace Avalonia.Media.TextFormatting.Unicode
[StructLayout(LayoutKind.Auto)] [StructLayout(LayoutKind.Auto)]
private ref struct Processor private ref struct Processor
{ {
private readonly ReadOnlySlice<char> _buffer; private readonly CharacterBufferRange _buffer;
private int _codeUnitLengthOfCurrentScalar; private int _codeUnitLengthOfCurrentScalar;
internal Processor(ReadOnlySlice<char> buffer) internal Processor(CharacterBufferRange buffer)
{ {
_buffer = buffer; _buffer = buffer;
_codeUnitLengthOfCurrentScalar = 0; _codeUnitLengthOfCurrentScalar = 0;

2
src/Avalonia.Base/Media/TextFormatting/Unicode/LineBreak.cs

@ -24,7 +24,7 @@ namespace Avalonia.Media.TextFormatting.Unicode
/// Information about a potential line break position /// Information about a potential line break position
/// </summary> /// </summary>
[DebuggerDisplay("{PositionMeasure}/{PositionWrap} @ {Required}")] [DebuggerDisplay("{PositionMeasure}/{PositionWrap} @ {Required}")]
public readonly struct LineBreak public readonly record struct LineBreak
{ {
/// <summary> /// <summary>
/// Constructor /// Constructor

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

Loading…
Cancel
Save