Browse Source

fix: merge conflict with master

pull/9628/head
Giuseppe Lippolis 4 years ago
parent
commit
940c871575
  1. 5
      .ncrunch/ReactiveUIDemo.v3.ncrunchproject
  2. 1
      Avalonia.Desktop.slnf
  3. 7
      Avalonia.sln
  4. 9
      native/Avalonia.Native/src/OSX/WindowImpl.mm
  5. 3
      samples/ControlCatalog/MainView.xaml
  6. 27
      samples/ControlCatalog/Pages/RefreshContainerPage.axaml
  7. 36
      samples/ControlCatalog/Pages/RefreshContainerPage.axaml.cs
  8. 26
      samples/ControlCatalog/ViewModels/RefreshContainerViewModel.cs
  9. 15
      samples/IntegrationTestApp/MainWindow.axaml
  10. 3
      samples/IntegrationTestApp/MainWindow.axaml.cs
  11. 8
      samples/IntegrationTestApp/ShowWindowTest.axaml
  12. 8
      samples/ReactiveUIDemo/App.axaml
  13. 37
      samples/ReactiveUIDemo/App.axaml.cs
  14. 19
      samples/ReactiveUIDemo/MainWindow.axaml
  15. 22
      samples/ReactiveUIDemo/MainWindow.axaml.cs
  16. 28
      samples/ReactiveUIDemo/ReactiveUIDemo.csproj
  17. 11
      samples/ReactiveUIDemo/ViewModels/BarViewModel.cs
  18. 11
      samples/ReactiveUIDemo/ViewModels/FooViewModel.cs
  19. 9
      samples/ReactiveUIDemo/ViewModels/MainWindowViewModel.cs
  20. 21
      samples/ReactiveUIDemo/ViewModels/RoutedViewHostPageViewModel.cs
  21. 16
      samples/ReactiveUIDemo/Views/BarView.axaml
  22. 28
      samples/ReactiveUIDemo/Views/BarView.axaml.cs
  23. 16
      samples/ReactiveUIDemo/Views/FooView.axaml
  24. 28
      samples/ReactiveUIDemo/Views/FooView.axaml.cs
  25. 2
      samples/RenderDemo/Pages/TextFormatterPage.axaml.cs
  26. 9
      src/Android/Avalonia.Android/AndroidPlatform.cs
  27. 32
      src/Android/Avalonia.Android/OpenGL/GlPlatformSurface.cs
  28. 30
      src/Android/Avalonia.Android/OpenGL/GlRenderTarget.cs
  29. 18
      src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs
  30. 3
      src/Avalonia.Base/Animation/Animators/Animator`1.cs
  31. 152
      src/Avalonia.Base/Input/GestureRecognizers/PullGestureRecognizer.cs
  32. 8
      src/Avalonia.Base/Input/Gestures.cs
  33. 5
      src/Avalonia.Base/Input/InputElement.cs
  34. 43
      src/Avalonia.Base/Input/PullGestureEventArgs.cs
  35. 8
      src/Avalonia.Base/Media/FormattedText.cs
  36. 419
      src/Avalonia.Base/Media/GlyphRun.cs
  37. 18
      src/Avalonia.Base/Media/GlyphRunMetrics.cs
  38. 2
      src/Avalonia.Base/Media/Imaging/RenderTargetBitmap.cs
  39. 293
      src/Avalonia.Base/Media/TextFormatting/CharacterBufferRange.cs
  40. 115
      src/Avalonia.Base/Media/TextFormatting/CharacterBufferReference.cs
  41. 13
      src/Avalonia.Base/Media/TextFormatting/FormattedTextSource.cs
  42. 19
      src/Avalonia.Base/Media/TextFormatting/InterWordJustification.cs
  43. 20
      src/Avalonia.Base/Media/TextFormatting/ShapeableTextCharacters.cs
  44. 31
      src/Avalonia.Base/Media/TextFormatting/ShapedBuffer.cs
  45. 19
      src/Avalonia.Base/Media/TextFormatting/ShapedTextCharacters.cs
  46. 4
      src/Avalonia.Base/Media/TextFormatting/SplitResult.cs
  47. 129
      src/Avalonia.Base/Media/TextFormatting/TextCharacters.cs
  48. 102
      src/Avalonia.Base/Media/TextFormatting/TextEllipsisHelper.cs
  49. 4
      src/Avalonia.Base/Media/TextFormatting/TextEndOfLine.cs
  50. 100
      src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs
  51. 2
      src/Avalonia.Base/Media/TextFormatting/TextLayout.cs
  52. 5
      src/Avalonia.Base/Media/TextFormatting/TextLeadingPrefixCharacterEllipsis.cs
  53. 358
      src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs
  54. 6
      src/Avalonia.Base/Media/TextFormatting/TextLineMetrics.cs
  55. 4
      src/Avalonia.Base/Media/TextFormatting/TextMetrics.cs
  56. 11
      src/Avalonia.Base/Media/TextFormatting/TextRun.cs
  57. 11
      src/Avalonia.Base/Media/TextFormatting/TextShaper.cs
  58. 3
      src/Avalonia.Base/Media/TextFormatting/TextTrailingCharacterEllipsis.cs
  59. 2
      src/Avalonia.Base/Media/TextFormatting/TextTrailingWordEllipsis.cs
  60. 3
      src/Avalonia.Base/Media/TextFormatting/Unicode/BiDiData.cs
  61. 9
      src/Avalonia.Base/Media/TextFormatting/Unicode/Codepoint.cs
  62. 7
      src/Avalonia.Base/Media/TextFormatting/Unicode/CodepointEnumerator.cs
  63. 8
      src/Avalonia.Base/Media/TextFormatting/Unicode/Grapheme.cs
  64. 12
      src/Avalonia.Base/Media/TextFormatting/Unicode/GraphemeEnumerator.cs
  65. 15
      src/Avalonia.Base/Media/TextFormatting/Unicode/LineBreakEnumerator.cs
  66. 11
      src/Avalonia.Base/Media/TextLeadingPrefixTrimming.cs
  67. 11
      src/Avalonia.Base/Media/TextTrailingTrimming.cs
  68. 2
      src/Avalonia.Base/Media/TextTrimming.cs
  69. 18
      src/Avalonia.Base/Platform/IOptionalFeatureProvider.cs
  70. 14
      src/Avalonia.Base/Platform/IPlatformGpu.cs
  71. 29
      src/Avalonia.Base/Platform/IPlatformRenderInterface.cs
  72. 10
      src/Avalonia.Base/Platform/IRenderTarget.cs
  73. 5
      src/Avalonia.Base/Platform/ITextShaperImpl.cs
  74. 4
      src/Avalonia.Base/Rendering/Composition/Animations/KeyFrameAnimationInstance.cs
  75. 9
      src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs
  76. 7
      src/Avalonia.Base/Rendering/Composition/Compositor.Factories.cs
  77. 33
      src/Avalonia.Base/Rendering/Composition/Compositor.cs
  78. 3
      src/Avalonia.Base/Rendering/Composition/Server/FpsCounter.cs
  79. 16
      src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs
  80. 54
      src/Avalonia.Base/Rendering/Composition/Server/ServerCompositor.cs
  81. 53
      src/Avalonia.Base/Rendering/DeferredRenderer.cs
  82. 6
      src/Avalonia.Base/Rendering/IRenderRoot.cs
  83. 6
      src/Avalonia.Base/Rendering/IRenderer.cs
  84. 26
      src/Avalonia.Base/Rendering/ImmediateRenderer.cs
  85. 24
      src/Avalonia.Base/Rendering/OwnedDisposable.cs
  86. 66
      src/Avalonia.Base/Rendering/PlatformRenderInterfaceContextManager.cs
  87. 26
      src/Avalonia.Base/Rendering/RenderLoop.cs
  88. 2
      src/Avalonia.Base/Rendering/SceneGraph/Scene.cs
  89. 67
      src/Avalonia.Base/StyledElement.cs
  90. 3
      src/Avalonia.Base/Styling/StyleInstance.cs
  91. 8
      src/Avalonia.Base/Utilities/ArraySlice.cs
  92. 239
      src/Avalonia.Base/Utilities/ReadOnlySlice.cs
  93. 90
      src/Avalonia.Base/Visual.cs
  94. 6
      src/Avalonia.Controls.DataGrid/DataGridTemplateColumn.cs
  95. 4
      src/Avalonia.Controls.DataGrid/Themes/Fluent.xaml
  96. 2
      src/Avalonia.Controls/AppBuilderBase.cs
  97. 5
      src/Avalonia.Controls/ComboBox.cs
  98. 85
      src/Avalonia.Controls/Control.cs
  99. 4
      src/Avalonia.Controls/Documents/LineBreak.cs
  100. 2
      src/Avalonia.Controls/Documents/Run.cs

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",

7
Avalonia.sln

@ -232,6 +232,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ControlCatalog.Browser", "s
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("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ControlCatalog.Browser.Blazor", "samples\ControlCatalog.Browser.Blazor\ControlCatalog.Browser.Blazor.csproj", "{90B08091-9BBD-4362-B712-E9F2CC62B218}"
EndProject EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ReactiveUIDemo", "samples\ReactiveUIDemo\ReactiveUIDemo.csproj", "{75C47156-C5D8-44BC-A5A7-E8657C2248D6}"
EndProject
Global Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU Debug|Any CPU = Debug|Any CPU
@ -543,6 +545,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
@ -607,6 +613,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}

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

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

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>

27
samples/ControlCatalog/Pages/RefreshContainerPage.axaml

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

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

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

26
samples/ControlCatalog/ViewModels/RefreshContainerViewModel.cs

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

15
samples/IntegrationTestApp/MainWindow.axaml

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

3
samples/IntegrationTestApp/MainWindow.axaml.cs

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

8
samples/IntegrationTestApp/ShowWindowTest.axaml

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

8
samples/ReactiveUIDemo/App.axaml

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

37
samples/ReactiveUIDemo/App.axaml.cs

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

19
samples/ReactiveUIDemo/MainWindow.axaml

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

22
samples/ReactiveUIDemo/MainWindow.axaml.cs

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

28
samples/ReactiveUIDemo/ReactiveUIDemo.csproj

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

11
samples/ReactiveUIDemo/ViewModels/BarViewModel.cs

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

11
samples/ReactiveUIDemo/ViewModels/FooViewModel.cs

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

9
samples/ReactiveUIDemo/ViewModels/MainWindowViewModel.cs

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

21
samples/ReactiveUIDemo/ViewModels/RoutedViewHostPageViewModel.cs

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

16
samples/ReactiveUIDemo/Views/BarView.axaml

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

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

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

16
samples/ReactiveUIDemo/Views/FooView.axaml

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

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

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

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

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

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);
}
}

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)

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);
}
} }
} }

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
}
}

8
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,

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()

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

@ -2,24 +2,30 @@
{ {
public readonly record 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/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;
} }
} }

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

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

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

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

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

@ -7,14 +7,15 @@ namespace Avalonia.Media.TextFormatting
{ {
internal readonly struct FormattedTextSource : ITextSource internal readonly struct FormattedTextSource : ITextSource
{ {
private readonly ReadOnlySlice<char> _text; private readonly CharacterBufferRange _text;
private readonly int length;
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 +36,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 +49,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 +123,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;
} }

31
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);
} }

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!);
} }

358
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/>
@ -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)
@ -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;

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

@ -6,13 +6,13 @@
/// </summary> /// </summary>
public readonly record 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.

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

@ -5,9 +5,9 @@
/// </summary> /// </summary>
public readonly record 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;

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);
} }
} }
} }

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);
} }
} }
} }

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);

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

@ -1,6 +1,5 @@
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
{ {
@ -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;
} }

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

@ -1,12 +1,13 @@
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;
private int _pos;
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 record 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;

15
src/Avalonia.Base/Media/TextFormatting/Unicode/LineBreakEnumerator.cs

@ -2,7 +2,8 @@
// 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 Avalonia.Utilities; using System;
using System.Collections.Generic;
namespace Avalonia.Media.TextFormatting.Unicode namespace Avalonia.Media.TextFormatting.Unicode
{ {
@ -12,7 +13,7 @@ namespace Avalonia.Media.TextFormatting.Unicode
/// </summary> /// </summary>
public ref struct LineBreakEnumerator public ref struct LineBreakEnumerator
{ {
private readonly ReadOnlySlice<char> _text; private readonly IReadOnlyList<char> _text;
private int _position; private int _position;
private int _lastPosition; private int _lastPosition;
private LineBreakClass _currentClass; private LineBreakClass _currentClass;
@ -28,7 +29,7 @@ namespace Avalonia.Media.TextFormatting.Unicode
private int _lb30a; private int _lb30a;
private bool _lb31; private bool _lb31;
public LineBreakEnumerator(ReadOnlySlice<char> text) public LineBreakEnumerator(IReadOnlyList<char> text)
: this() : this()
{ {
_text = text; _text = text;
@ -62,7 +63,7 @@ namespace Avalonia.Media.TextFormatting.Unicode
_lb30a = 0; _lb30a = 0;
} }
while (_position < _text.Length) while (_position < _text.Count)
{ {
_lastPosition = _position; _lastPosition = _position;
var lastClass = _nextClass; var lastClass = _nextClass;
@ -92,11 +93,11 @@ namespace Avalonia.Media.TextFormatting.Unicode
} }
} }
if (_position >= _text.Length) if (_position >= _text.Count)
{ {
if (_lastPosition < _text.Length) if (_lastPosition < _text.Count)
{ {
_lastPosition = _text.Length; _lastPosition = _text.Count;
var required = false; var required = false;

11
src/Avalonia.Base/Media/TextLeadingPrefixTrimming.cs

@ -1,21 +1,16 @@
using Avalonia.Media.TextFormatting; using Avalonia.Media.TextFormatting;
using Avalonia.Utilities;
namespace Avalonia.Media namespace Avalonia.Media
{ {
public sealed class TextLeadingPrefixTrimming : TextTrimming public sealed class TextLeadingPrefixTrimming : TextTrimming
{ {
private readonly ReadOnlySlice<char> _ellipsis; private readonly string _ellipsis;
private readonly int _prefixLength; private readonly int _prefixLength;
public TextLeadingPrefixTrimming(char ellipsis, int prefixLength) : this(new[] { ellipsis }, prefixLength) public TextLeadingPrefixTrimming(string ellipsis, int prefixLength)
{
}
public TextLeadingPrefixTrimming(char[] ellipsis, int prefixLength)
{ {
_prefixLength = prefixLength; _prefixLength = prefixLength;
_ellipsis = new ReadOnlySlice<char>(ellipsis); _ellipsis = ellipsis;
} }
public override TextCollapsingProperties CreateCollapsingProperties(TextCollapsingCreateInfo createInfo) public override TextCollapsingProperties CreateCollapsingProperties(TextCollapsingCreateInfo createInfo)

11
src/Avalonia.Base/Media/TextTrailingTrimming.cs

@ -1,21 +1,16 @@
using Avalonia.Media.TextFormatting; using Avalonia.Media.TextFormatting;
using Avalonia.Utilities;
namespace Avalonia.Media namespace Avalonia.Media
{ {
public sealed class TextTrailingTrimming : TextTrimming public sealed class TextTrailingTrimming : TextTrimming
{ {
private readonly ReadOnlySlice<char> _ellipsis; private readonly string _ellipsis;
private readonly bool _isWordBased; private readonly bool _isWordBased;
public TextTrailingTrimming(char ellipsis, bool isWordBased) : this(new[] {ellipsis}, isWordBased)
{
}
public TextTrailingTrimming(char[] ellipsis, bool isWordBased) public TextTrailingTrimming(string ellipsis, bool isWordBased)
{ {
_isWordBased = isWordBased; _isWordBased = isWordBased;
_ellipsis = new ReadOnlySlice<char>(ellipsis); _ellipsis = ellipsis;
} }
public override TextCollapsingProperties CreateCollapsingProperties(TextCollapsingCreateInfo createInfo) public override TextCollapsingProperties CreateCollapsingProperties(TextCollapsingCreateInfo createInfo)

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

@ -8,7 +8,7 @@ namespace Avalonia.Media
/// </summary> /// </summary>
public abstract class TextTrimming public abstract class TextTrimming
{ {
internal const char DefaultEllipsisChar = '\u2026'; internal const string DefaultEllipsisChar = "\u2026";
/// <summary> /// <summary>
/// Text is not trimmed. /// Text is not trimmed.

18
src/Avalonia.Base/Platform/IOptionalFeatureProvider.cs

@ -0,0 +1,18 @@
using System;
namespace Avalonia.Platform;
public interface IOptionalFeatureProvider
{
/// <summary>
/// Queries for an optional feature
/// </summary>
/// <param name="featureType">Feature type</param>
public object? TryGetFeature(Type featureType);
}
public static class OptionalFeatureProviderExtensions
{
public static T? TryGetFeature<T>(this IOptionalFeatureProvider provider) where T : class =>
(T?)provider.TryGetFeature(typeof(T));
}

14
src/Avalonia.Base/Platform/IPlatformGpu.cs

@ -4,13 +4,21 @@ using Avalonia.Metadata;
namespace Avalonia.Platform; namespace Avalonia.Platform;
[Unstable] [Unstable]
public interface IPlatformGpu public interface IPlatformGraphics
{ {
IPlatformGpuContext PrimaryContext { get; } bool UsesSharedContext { get; }
IPlatformGraphicsContext CreateContext();
IPlatformGraphicsContext GetSharedContext();
} }
[Unstable] [Unstable]
public interface IPlatformGpuContext : IDisposable public interface IPlatformGraphicsContext : IDisposable, IOptionalFeatureProvider
{ {
bool IsLost { get; }
IDisposable EnsureCurrent(); IDisposable EnsureCurrent();
}
public class PlatformGraphicsContextLostException : Exception
{
} }

29
src/Avalonia.Base/Platform/IPlatformRenderInterface.cs

@ -65,15 +65,6 @@ namespace Avalonia.Platform
/// <returns>The geometry returned contains the combined geometry of all glyphs in the glyph run.</returns> /// <returns>The geometry returned contains the combined geometry of all glyphs in the glyph run.</returns>
IGeometryImpl BuildGlyphRunGeometry(GlyphRun glyphRun); IGeometryImpl BuildGlyphRunGeometry(GlyphRun glyphRun);
/// <summary>
/// Creates a renderer.
/// </summary>
/// <param name="surfaces">
/// The list of native platform surfaces that can be used for output.
/// </param>
/// <returns>An <see cref="IRenderTarget"/>.</returns>
IRenderTarget CreateRenderTarget(IEnumerable<object> surfaces);
/// <summary> /// <summary>
/// Creates a render target bitmap implementation. /// Creates a render target bitmap implementation.
/// </summary> /// </summary>
@ -181,6 +172,13 @@ namespace Avalonia.Platform
/// <returns></returns> /// <returns></returns>
IGlyphRunImpl CreateGlyphRun(IGlyphTypeface glyphTypeface, double fontRenderingEmSize, IReadOnlyList<ushort> glyphIndices, IReadOnlyList<double>? glyphAdvances, IReadOnlyList<Vector>? glyphOffsets); IGlyphRunImpl CreateGlyphRun(IGlyphTypeface glyphTypeface, double fontRenderingEmSize, IReadOnlyList<ushort> glyphIndices, IReadOnlyList<double>? glyphAdvances, IReadOnlyList<Vector>? glyphOffsets);
/// <summary>
/// Creates a backend-specific object using a low-level API graphics context
/// </summary>
/// <param name="graphicsApiContext">An underlying low-level graphics context (e. g. wrapped OpenGL context, Vulkan device, D3DDevice, etc)</param>
/// <returns></returns>
IPlatformRenderInterfaceContext CreateBackendContext(IPlatformGraphicsContext? graphicsApiContext);
/// <summary> /// <summary>
/// Gets a value indicating whether the platform directly supports rectangles with rounded corners. /// Gets a value indicating whether the platform directly supports rectangles with rounded corners.
/// </summary> /// </summary>
@ -200,4 +198,17 @@ namespace Avalonia.Platform
/// </summary> /// </summary>
public PixelFormat DefaultPixelFormat { get; } public PixelFormat DefaultPixelFormat { get; }
} }
[Unstable]
public interface IPlatformRenderInterfaceContext : IOptionalFeatureProvider, IDisposable
{
/// <summary>
/// Creates a renderer.
/// </summary>
/// <param name="surfaces">
/// The list of native platform surfaces that can be used for output.
/// </param>
/// <returns>An <see cref="IRenderTarget"/>.</returns>
IRenderTarget CreateRenderTarget(IEnumerable<object> surfaces);
}
} }

10
src/Avalonia.Base/Platform/IRenderTarget.cs

@ -19,10 +19,10 @@ namespace Avalonia.Platform
/// to be drawn. /// to be drawn.
/// </param> /// </param>
IDrawingContextImpl CreateDrawingContext(IVisualBrushRenderer? visualBrushRenderer); IDrawingContextImpl CreateDrawingContext(IVisualBrushRenderer? visualBrushRenderer);
}
/// <summary>
public interface IRenderTargetWithCorruptionInfo : IRenderTarget /// Indicates if the render target is no longer usable and needs to be recreated
{ /// </summary>
bool IsCorrupted { get; } public bool IsCorrupted { get; }
} }
} }

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

@ -1,6 +1,5 @@
using Avalonia.Media.TextFormatting; using Avalonia.Media.TextFormatting;
using Avalonia.Metadata; using Avalonia.Metadata;
using Avalonia.Utilities;
namespace Avalonia.Platform namespace Avalonia.Platform
{ {
@ -13,9 +12,9 @@ namespace Avalonia.Platform
/// <summary> /// <summary>
/// Shapes the specified region within the text and returns a shaped buffer. /// Shapes the specified region within the text and returns a shaped buffer.
/// </summary> /// </summary>
/// <param name="text">The text.</param> /// <param name="text">The text buffer.</param>
/// <param name="options">Text shaper options to customize the shaping process.</param> /// <param name="options">Text shaper options to customize the shaping process.</param>
/// <returns>A shaped glyph run.</returns> /// <returns>A shaped glyph run.</returns>
ShapedBuffer ShapeText(ReadOnlySlice<char> text, TextShaperOptions options); ShapedBuffer ShapeText(CharacterBufferReference text, int length, TextShaperOptions options);
} }
} }

4
src/Avalonia.Base/Rendering/Composition/Animations/KeyFrameAnimationInstance.cs

@ -175,6 +175,10 @@ namespace Avalonia.Rendering.Composition.Animations
public override void Activate() public override void Activate()
{ {
if (_finished)
{
return;
}
TargetObject.Compositor.AddToClock(this); TargetObject.Compositor.AddToClock(this);
base.Activate(); base.Activate();
} }

9
src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs

@ -38,13 +38,12 @@ public class CompositingRenderer : IRendererWithCompositor
/// </summary> /// </summary>
public bool RenderOnlyOnRenderThread { get; set; } = true; public bool RenderOnlyOnRenderThread { get; set; } = true;
public CompositingRenderer(IRenderRoot root, public CompositingRenderer(IRenderRoot root, Compositor compositor, Func<IEnumerable<object>> surfaces)
Compositor compositor)
{ {
_root = root; _root = root;
_compositor = compositor; _compositor = compositor;
_recordingContext = new DrawingContext(_recorder); _recordingContext = new DrawingContext(_recorder);
CompositionTarget = compositor.CreateCompositionTarget(root.CreateRenderTarget); CompositionTarget = compositor.CreateCompositionTarget(surfaces);
CompositionTarget.Root = ((Visual)root).AttachToCompositor(compositor); CompositionTarget.Root = ((Visual)root).AttachToCompositor(compositor);
_update = Update; _update = Update;
} }
@ -301,7 +300,9 @@ public class CompositingRenderer : IRendererWithCompositor
{ {
CompositionTarget.IsEnabled = false; CompositionTarget.IsEnabled = false;
} }
public ValueTask<object?> TryGetRenderInterfaceFeature(Type featureType) => Compositor.TryGetRenderInterfaceFeature(featureType);
public void Dispose() public void Dispose()
{ {
Stop(); Stop();

7
src/Avalonia.Base/Rendering/Composition/Compositor.Factories.cs

@ -1,4 +1,5 @@
using System; using System;
using System.Collections.Generic;
using Avalonia.Platform; using Avalonia.Platform;
using Avalonia.Rendering.Composition.Animations; using Avalonia.Rendering.Composition.Animations;
using Avalonia.Rendering.Composition.Server; using Avalonia.Rendering.Composition.Server;
@ -10,11 +11,11 @@ public partial class Compositor
/// <summary> /// <summary>
/// Creates a new CompositionTarget /// Creates a new CompositionTarget
/// </summary> /// </summary>
/// <param name="renderTargetFactory">A factory method to create IRenderTarget to be called from the render thread</param> /// <param name="surfaces">A factory method to create IRenderTarget to be called from the render thread</param>
/// <returns></returns> /// <returns></returns>
public CompositionTarget CreateCompositionTarget(Func<IRenderTarget> renderTargetFactory) public CompositionTarget CreateCompositionTarget(Func<IEnumerable<object>> surfaces)
{ {
return new CompositionTarget(this, new ServerCompositionTarget(_server, renderTargetFactory)); return new CompositionTarget(this, new ServerCompositionTarget(_server, surfaces));
} }
public CompositionContainerVisual CreateContainerVisual() => new(this, new ServerCompositionContainerVisual(_server)); public CompositionContainerVisual CreateContainerVisual() => new(this, new ServerCompositionContainerVisual(_server));

33
src/Avalonia.Base/Rendering/Composition/Compositor.cs

@ -34,6 +34,7 @@ namespace Avalonia.Rendering.Composition
internal ServerCompositor Server => _server; internal ServerCompositor Server => _server;
private Task? _pendingBatch; private Task? _pendingBatch;
private readonly object _pendingBatchLock = new(); private readonly object _pendingBatchLock = new();
private List<Action> _pendingServerCompositorJobs = new();
internal IEasing DefaultEasing { get; } internal IEasing DefaultEasing { get; }
@ -43,7 +44,7 @@ namespace Avalonia.Rendering.Composition
/// </summary> /// </summary>
/// <param name="loop"></param> /// <param name="loop"></param>
/// <param name="gpu"></param> /// <param name="gpu"></param>
public Compositor(IRenderLoop loop, IPlatformGpu? gpu) public Compositor(IRenderLoop loop, IPlatformGraphics? gpu)
{ {
Loop = loop; Loop = loop;
_server = new ServerCompositor(loop, gpu, _batchObjectPool, _batchMemoryPool); _server = new ServerCompositor(loop, gpu, _batchObjectPool, _batchMemoryPool);
@ -101,6 +102,13 @@ namespace Avalonia.Rendering.Composition
#endif #endif
} }
_objectsForSerialization.Clear(); _objectsForSerialization.Clear();
if (_pendingServerCompositorJobs.Count > 0)
{
writer.WriteObject(ServerCompositor.RenderThreadJobsStartMarker);
foreach (var job in _pendingServerCompositorJobs)
writer.WriteObject(job);
writer.WriteObject(ServerCompositor.RenderThreadJobsEndMarker);
}
} }
batch.CommitedAt = Server.Clock.Elapsed; batch.CommitedAt = Server.Clock.Elapsed;
@ -136,5 +144,28 @@ namespace Avalonia.Rendering.Composition
_invokeBeforeCommit.Enqueue(action); _invokeBeforeCommit.Enqueue(action);
RequestCommitAsync(); RequestCommitAsync();
} }
/// <summary>
/// Attempts to query for a feature from the platform render interface
/// </summary>
public ValueTask<object?> TryGetRenderInterfaceFeature(Type featureType)
{
var tcs = new TaskCompletionSource<object?>();
_pendingServerCompositorJobs.Add(() =>
{
try
{
using (Server.RenderInterface.EnsureCurrent())
{
tcs.TrySetResult(Server.RenderInterface.Value.TryGetFeature(featureType));
}
}
catch (Exception e)
{
tcs.TrySetException(e);
}
});
return new ValueTask<object?>(tcs.Task);
}
} }
} }

3
src/Avalonia.Base/Rendering/Composition/Server/FpsCounter.cs

@ -1,6 +1,7 @@
using System; using System;
using System.Diagnostics; using System.Diagnostics;
using System.Globalization; using System.Globalization;
using System.Linq;
using Avalonia.Media; using Avalonia.Media;
using Avalonia.Media.TextFormatting; using Avalonia.Media.TextFormatting;
using Avalonia.Platform; using Avalonia.Platform;
@ -31,7 +32,7 @@ internal class FpsCounter
{ {
var s = new string((char)c, 1); var s = new string((char)c, 1);
var glyph = typeface.GetGlyph((uint)(s[0])); var glyph = typeface.GetGlyph((uint)(s[0]));
_runs[c - FirstChar] = new GlyphRun(typeface, 18, new ReadOnlySlice<char>(s.AsMemory()), new ushort[] { glyph }); _runs[c - FirstChar] = new GlyphRun(typeface, 18, s.ToArray(), new ushort[] { glyph });
} }
} }

16
src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs

@ -20,7 +20,7 @@ namespace Avalonia.Rendering.Composition.Server
internal partial class ServerCompositionTarget : IDisposable internal partial class ServerCompositionTarget : IDisposable
{ {
private readonly ServerCompositor _compositor; private readonly ServerCompositor _compositor;
private readonly Func<IRenderTarget> _renderTargetFactory; private readonly Func<IEnumerable<object>> _surfaces;
private static long s_nextId = 1; private static long s_nextId = 1;
public long Id { get; } public long Id { get; }
public ulong Revision { get; private set; } public ulong Revision { get; private set; }
@ -39,11 +39,11 @@ namespace Avalonia.Rendering.Composition.Server
public ReadbackIndices Readback { get; } = new(); public ReadbackIndices Readback { get; } = new();
public int RenderedVisuals { get; set; } public int RenderedVisuals { get; set; }
public ServerCompositionTarget(ServerCompositor compositor, Func<IRenderTarget> renderTargetFactory) : public ServerCompositionTarget(ServerCompositor compositor, Func<IEnumerable<object>> surfaces) :
base(compositor) base(compositor)
{ {
_compositor = compositor; _compositor = compositor;
_renderTargetFactory = renderTargetFactory; _surfaces = surfaces;
Id = Interlocked.Increment(ref s_nextId); Id = Interlocked.Increment(ref s_nextId);
} }
@ -79,13 +79,14 @@ namespace Avalonia.Rendering.Composition.Server
if (Root == null) if (Root == null)
return; return;
if ((_renderTarget as IRenderTargetWithCorruptionInfo)?.IsCorrupted == true) if (_renderTarget?.IsCorrupted == true)
{ {
_renderTarget!.Dispose(); _renderTarget!.Dispose();
_renderTarget = null; _renderTarget = null;
_redrawRequested = true;
} }
_renderTarget ??= _renderTargetFactory(); _renderTarget ??= _compositor.CreateRenderTarget(_surfaces());
Compositor.UpdateServerTime(); Compositor.UpdateServerTime();
@ -109,12 +110,13 @@ namespace Avalonia.Rendering.Composition.Server
using (var targetContext = _renderTarget.CreateDrawingContext(null)) using (var targetContext = _renderTarget.CreateDrawingContext(null))
{ {
var layerSize = Size * Scaling; var layerSize = Size * Scaling;
if (layerSize != _layerSize || _layer == null) if (layerSize != _layerSize || _layer == null || _layer.IsCorrupted)
{ {
_layer?.Dispose(); _layer?.Dispose();
_layer = null; _layer = null;
_layer = targetContext.CreateLayer(Size); _layer = targetContext.CreateLayer(Size);
_layerSize = layerSize; _layerSize = layerSize;
_dirtyRect = new Rect(0, 0, layerSize.Width, layerSize.Height);
} }
if (!_dirtyRect.IsEmpty) if (!_dirtyRect.IsEmpty)
@ -197,7 +199,7 @@ namespace Avalonia.Rendering.Composition.Server
if(_disposed) if(_disposed)
return; return;
_disposed = true; _disposed = true;
using (_compositor.GpuContext?.EnsureCurrent()) using (_compositor.RenderInterface.EnsureCurrent())
{ {
if (_layer != null) if (_layer != null)
{ {

54
src/Avalonia.Base/Rendering/Composition/Server/ServerCompositor.cs

@ -1,6 +1,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics; using System.Diagnostics;
using Avalonia.Logging;
using Avalonia.Platform; using Avalonia.Platform;
using Avalonia.Rendering.Composition.Animations; using Avalonia.Rendering.Composition.Animations;
using Avalonia.Rendering.Composition.Expressions; using Avalonia.Rendering.Composition.Expressions;
@ -19,6 +20,7 @@ namespace Avalonia.Rendering.Composition.Server
internal class ServerCompositor : IRenderLoopTask internal class ServerCompositor : IRenderLoopTask
{ {
private readonly IRenderLoop _renderLoop; private readonly IRenderLoop _renderLoop;
private readonly Queue<Batch> _batches = new Queue<Batch>(); private readonly Queue<Batch> _batches = new Queue<Batch>();
public long LastBatchId { get; private set; } public long LastBatchId { get; private set; }
public Stopwatch Clock { get; } = Stopwatch.StartNew(); public Stopwatch Clock { get; } = Stopwatch.StartNew();
@ -29,13 +31,15 @@ namespace Avalonia.Rendering.Composition.Server
internal BatchStreamObjectPool<object?> BatchObjectPool; internal BatchStreamObjectPool<object?> BatchObjectPool;
internal BatchStreamMemoryPool BatchMemoryPool; internal BatchStreamMemoryPool BatchMemoryPool;
private object _lock = new object(); private object _lock = new object();
public IPlatformGpuContext? GpuContext { get; } public PlatformRenderInterfaceContextManager RenderInterface { get; }
internal static readonly object RenderThreadJobsStartMarker = new();
internal static readonly object RenderThreadJobsEndMarker = new();
public ServerCompositor(IRenderLoop renderLoop, IPlatformGpu? platformGpu, public ServerCompositor(IRenderLoop renderLoop, IPlatformGraphics? platformGraphics,
BatchStreamObjectPool<object?> batchObjectPool, BatchStreamMemoryPool batchMemoryPool) BatchStreamObjectPool<object?> batchObjectPool, BatchStreamMemoryPool batchMemoryPool)
{ {
GpuContext = platformGpu?.PrimaryContext;
_renderLoop = renderLoop; _renderLoop = renderLoop;
RenderInterface = new PlatformRenderInterfaceContextManager(platformGraphics);
BatchObjectPool = batchObjectPool; BatchObjectPool = batchObjectPool;
BatchMemoryPool = batchMemoryPool; BatchMemoryPool = batchMemoryPool;
_renderLoop.Add(this); _renderLoop.Add(this);
@ -66,7 +70,14 @@ namespace Avalonia.Rendering.Composition.Server
{ {
while (!stream.IsObjectEof) while (!stream.IsObjectEof)
{ {
var target = (ServerObject)stream.ReadObject()!; var readObject = stream.ReadObject();
if (readObject == RenderThreadJobsStartMarker)
{
ReadAndExecuteJobs(stream);
continue;
}
var target = (ServerObject)readObject!;
target.DeserializeChanges(stream, batch); target.DeserializeChanges(stream, batch);
#if DEBUG_COMPOSITOR_SERIALIZATION #if DEBUG_COMPOSITOR_SERIALIZATION
if (stream.ReadObject() != BatchStreamDebugMarkers.ObjectEndMarker) if (stream.ReadObject() != BatchStreamDebugMarkers.ObjectEndMarker)
@ -84,6 +95,23 @@ namespace Avalonia.Rendering.Composition.Server
} }
} }
void ReadAndExecuteJobs(BatchStreamReader reader)
{
object? readObject;
while ((readObject = reader.ReadObject()) != RenderThreadJobsEndMarker)
{
var job = (Action)readObject!;
try
{
job();
}
catch
{
// Ignore
}
}
}
void CompletePendingBatches() void CompletePendingBatches()
{ {
foreach(var batch in _reusableToCompleteList) foreach(var batch in _reusableToCompleteList)
@ -118,8 +146,16 @@ namespace Avalonia.Rendering.Composition.Server
_animationsToUpdate.Clear(); _animationsToUpdate.Clear();
foreach (var t in _activeTargets) try
t.Render(); {
RenderInterface.EnsureValidBackendContext();
foreach (var t in _activeTargets)
t.Render();
}
catch (Exception e)
{
Logger.TryGet(LogEventLevel.Error, LogArea.Visual)?.Log(this, "Exception when rendering: {Error}", e);
}
} }
public void AddCompositionTarget(ServerCompositionTarget target) public void AddCompositionTarget(ServerCompositionTarget target)
@ -137,5 +173,11 @@ namespace Avalonia.Rendering.Composition.Server
public void RemoveFromClock(IAnimationInstance animationInstance) => public void RemoveFromClock(IAnimationInstance animationInstance) =>
_activeAnimations.Remove(animationInstance); _activeAnimations.Remove(animationInstance);
public IRenderTarget CreateRenderTarget(IEnumerable<object> surfaces)
{
using (RenderInterface.EnsureCurrent())
return RenderInterface.CreateRenderTarget(surfaces);
}
} }
} }

53
src/Avalonia.Base/Rendering/DeferredRenderer.cs

@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Threading.Tasks;
using Avalonia.Logging; using Avalonia.Logging;
using Avalonia.Media; using Avalonia.Media;
using Avalonia.Media.Immutable; using Avalonia.Media.Immutable;
@ -22,6 +23,8 @@ namespace Avalonia.Rendering
{ {
private readonly IDispatcher? _dispatcher; private readonly IDispatcher? _dispatcher;
private readonly IRenderLoop? _renderLoop; private readonly IRenderLoop? _renderLoop;
private readonly Func<IRenderTarget>? _renderTargetFactory;
private readonly PlatformRenderInterfaceContextManager? _renderInterface;
private readonly Visual _root; private readonly Visual _root;
private readonly ISceneBuilder _sceneBuilder; private readonly ISceneBuilder _sceneBuilder;
@ -39,6 +42,7 @@ namespace Avalonia.Rendering
private readonly object _startStopLock = new object(); private readonly object _startStopLock = new object();
private readonly object _renderLoopIsRenderingLock = new object(); private readonly object _renderLoopIsRenderingLock = new object();
private readonly Action _updateSceneIfNeededDelegate; private readonly Action _updateSceneIfNeededDelegate;
private List<Action>? _pendingRenderThreadJobs;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="DeferredRenderer"/> class. /// Initializes a new instance of the <see cref="DeferredRenderer"/> class.
@ -51,6 +55,8 @@ namespace Avalonia.Rendering
public DeferredRenderer( public DeferredRenderer(
IRenderRoot root, IRenderRoot root,
IRenderLoop renderLoop, IRenderLoop renderLoop,
Func<IRenderTarget> renderTargetFactory,
PlatformRenderInterfaceContextManager? renderInterface = null,
ISceneBuilder? sceneBuilder = null, ISceneBuilder? sceneBuilder = null,
IDispatcher? dispatcher = null, IDispatcher? dispatcher = null,
IDeferredRendererLock? rendererLock = null) : base(true) IDeferredRendererLock? rendererLock = null) : base(true)
@ -60,6 +66,8 @@ namespace Avalonia.Rendering
_sceneBuilder = sceneBuilder ?? new SceneBuilder(); _sceneBuilder = sceneBuilder ?? new SceneBuilder();
Layers = new RenderLayers(); Layers = new RenderLayers();
_renderLoop = renderLoop; _renderLoop = renderLoop;
_renderTargetFactory = renderTargetFactory;
_renderInterface = renderInterface;
_lock = rendererLock ?? new ManagedDeferredRendererLock(); _lock = rendererLock ?? new ManagedDeferredRendererLock();
_updateSceneIfNeededDelegate = UpdateSceneIfNeeded; _updateSceneIfNeededDelegate = UpdateSceneIfNeeded;
} }
@ -256,6 +264,30 @@ namespace Avalonia.Rendering
} }
} }
public ValueTask<object?> TryGetRenderInterfaceFeature(Type featureType)
{
if (_renderInterface == null)
return new((object?)null);
var tcs = new TaskCompletionSource<object?>();
_pendingRenderThreadJobs ??= new();
_pendingRenderThreadJobs.Add(() =>
{
try
{
using (_renderInterface.EnsureCurrent())
{
tcs.TrySetResult(_renderInterface.Value.TryGetFeature(featureType));
}
}
catch (Exception e)
{
tcs.TrySetException(e);
}
});
return new ValueTask<object?>(tcs.Task);
}
bool NeedsUpdate => _dirty == null || _dirty.Count > 0; bool NeedsUpdate => _dirty == null || _dirty.Count > 0;
bool IRenderLoopTask.NeedsUpdate => NeedsUpdate; bool IRenderLoopTask.NeedsUpdate => NeedsUpdate;
@ -337,7 +369,16 @@ namespace Avalonia.Rendering
} }
finally finally
{ {
scene.Item.MarkAsRendered(); try
{
if(scene.Item.RenderThreadJobs!=null)
foreach (var job in scene.Item.RenderThreadJobs)
job();
}
finally
{
scene.Item.MarkAsRendered();
}
} }
} }
} }
@ -604,7 +645,7 @@ namespace Avalonia.Rendering
return; return;
} }
if ((RenderTarget as IRenderTargetWithCorruptionInfo)?.IsCorrupted == true) if (RenderTarget?.IsCorrupted == true)
{ {
RenderTarget!.Dispose(); RenderTarget!.Dispose();
RenderTarget = null; RenderTarget = null;
@ -612,7 +653,7 @@ namespace Avalonia.Rendering
if (RenderTarget == null) if (RenderTarget == null)
{ {
RenderTarget = ((IRenderRoot)_root).CreateRenderTarget(); RenderTarget = _renderTargetFactory!();
} }
context = RenderTarget.CreateDrawingContext(this); context = RenderTarget.CreateDrawingContext(this);
@ -637,7 +678,11 @@ namespace Avalonia.Rendering
} }
if (_root.IsVisible) if (_root.IsVisible)
{ {
var sceneRef = RefCountable.Create(_scene?.Item.CloneScene() ?? new Scene(_root)); var sceneRef = RefCountable.Create(_scene?.Item.CloneScene() ?? new Scene(_root)
{
RenderThreadJobs = _pendingRenderThreadJobs
});
_pendingRenderThreadJobs = null;
var scene = sceneRef.Item; var scene = sceneRef.Item;
if (_dirty == null) if (_dirty == null)

6
src/Avalonia.Base/Rendering/IRenderRoot.cs

@ -25,12 +25,6 @@ namespace Avalonia.Rendering
/// </summary> /// </summary>
double RenderScaling { get; } double RenderScaling { get; }
/// <summary>
/// Creates a render target for the window.
/// </summary>
/// <returns>An <see cref="IRenderTarget"/>.</returns>
IRenderTarget CreateRenderTarget();
/// <summary> /// <summary>
/// Adds a rectangle to the window's dirty region. /// Adds a rectangle to the window's dirty region.
/// </summary> /// </summary>

6
src/Avalonia.Base/Rendering/IRenderer.cs

@ -1,6 +1,7 @@
using System; using System;
using Avalonia.VisualTree; using Avalonia.VisualTree;
using System.Collections.Generic; using System.Collections.Generic;
using System.Threading.Tasks;
using Avalonia.Rendering.Composition; using Avalonia.Rendering.Composition;
namespace Avalonia.Rendering namespace Avalonia.Rendering
@ -87,6 +88,11 @@ namespace Avalonia.Rendering
/// Stops the renderer. /// Stops the renderer.
/// </summary> /// </summary>
void Stop(); void Stop();
/// <summary>
/// Attempts to query for a feature from the platform render interface
/// </summary>
public ValueTask<object?> TryGetRenderInterfaceFeature(Type featureType);
} }
public interface IRendererWithCompositor : IRenderer public interface IRendererWithCompositor : IRenderer

26
src/Avalonia.Base/Rendering/ImmediateRenderer.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.Threading.Tasks;
using Avalonia.Logging; using Avalonia.Logging;
using Avalonia.Media; using Avalonia.Media;
using Avalonia.Platform; using Avalonia.Platform;
@ -19,6 +20,8 @@ namespace Avalonia.Rendering
public class ImmediateRenderer : RendererBase, IRenderer, IVisualBrushRenderer public class ImmediateRenderer : RendererBase, IRenderer, IVisualBrushRenderer
{ {
private readonly Visual _root; private readonly Visual _root;
private readonly Func<IRenderTarget> _renderTargetFactory;
private readonly PlatformRenderInterfaceContextManager? _renderContext;
private readonly IRenderRoot? _renderRoot; private readonly IRenderRoot? _renderRoot;
private bool _updateTransformedBounds = true; private bool _updateTransformedBounds = true;
private IRenderTarget? _renderTarget; private IRenderTarget? _renderTarget;
@ -27,15 +30,19 @@ namespace Avalonia.Rendering
/// Initializes a new instance of the <see cref="ImmediateRenderer"/> class. /// Initializes a new instance of the <see cref="ImmediateRenderer"/> class.
/// </summary> /// </summary>
/// <param name="root">The control to render.</param> /// <param name="root">The control to render.</param>
public ImmediateRenderer(Visual root) public ImmediateRenderer(Visual root, Func<IRenderTarget> renderTargetFactory,
PlatformRenderInterfaceContextManager? renderContext = null)
{ {
_root = root ?? throw new ArgumentNullException(nameof(root)); _root = root ?? throw new ArgumentNullException(nameof(root));
_renderTargetFactory = renderTargetFactory;
_renderContext = renderContext;
_renderRoot = root as IRenderRoot; _renderRoot = root as IRenderRoot;
} }
private ImmediateRenderer(Visual root, bool updateTransformedBounds) private ImmediateRenderer(Visual root, Func<IRenderTarget> renderTargetFactory, bool updateTransformedBounds)
{ {
_root = root ?? throw new ArgumentNullException(nameof(root)); _root = root ?? throw new ArgumentNullException(nameof(root));
_renderTargetFactory = renderTargetFactory;
_renderRoot = root as IRenderRoot; _renderRoot = root as IRenderRoot;
_updateTransformedBounds = updateTransformedBounds; _updateTransformedBounds = updateTransformedBounds;
} }
@ -54,7 +61,7 @@ namespace Avalonia.Rendering
{ {
if (_renderTarget == null) if (_renderTarget == null)
{ {
_renderTarget = ((IRenderRoot)_root).CreateRenderTarget(); _renderTarget = _renderTargetFactory();
} }
try try
@ -104,7 +111,7 @@ namespace Avalonia.Rendering
/// <param name="target">The render target.</param> /// <param name="target">The render target.</param>
public static void Render(Visual visual, IRenderTarget target) public static void Render(Visual visual, IRenderTarget target)
{ {
using (var renderer = new ImmediateRenderer(visual, updateTransformedBounds: false)) using (var renderer = new ImmediateRenderer(visual, () => target, updateTransformedBounds: false))
using (var context = new DrawingContext(target.CreateDrawingContext(renderer))) using (var context = new DrawingContext(target.CreateDrawingContext(renderer)))
{ {
renderer.Render(context, visual, visual.Bounds); renderer.Render(context, visual, visual.Bounds);
@ -118,7 +125,9 @@ namespace Avalonia.Rendering
/// <param name="context">The drawing context.</param> /// <param name="context">The drawing context.</param>
public static void Render(Visual visual, DrawingContext context) public static void Render(Visual visual, DrawingContext context)
{ {
using (var renderer = new ImmediateRenderer(visual, updateTransformedBounds: false)) using (var renderer = new ImmediateRenderer(visual,
() => throw new InvalidOperationException("This is not supposed to be called"),
updateTransformedBounds: false))
{ {
renderer.Render(context, visual, visual.Bounds); renderer.Render(context, visual, visual.Bounds);
} }
@ -185,6 +194,9 @@ namespace Avalonia.Rendering
{ {
} }
public ValueTask<object?> TryGetRenderInterfaceFeature(Type featureType) =>
new(_renderContext?.Value?.TryGetFeature(featureType));
/// <inheritdoc/> /// <inheritdoc/>
Size IVisualBrushRenderer.GetRenderTargetSize(IVisualBrush brush) Size IVisualBrushRenderer.GetRenderTargetSize(IVisualBrush brush)
{ {
@ -201,7 +213,9 @@ namespace Avalonia.Rendering
internal static void Render(Visual visual, DrawingContext context, bool updateTransformedBounds) internal static void Render(Visual visual, DrawingContext context, bool updateTransformedBounds)
{ {
using var renderer = new ImmediateRenderer(visual, updateTransformedBounds); using var renderer = new ImmediateRenderer(visual,
() => throw new InvalidOperationException("This is not supposed to be called"),
updateTransformedBounds);
renderer.Render(context, visual, visual.Bounds); renderer.Render(context, visual, visual.Bounds);
} }

24
src/Avalonia.Base/Rendering/OwnedDisposable.cs

@ -0,0 +1,24 @@
using System;
namespace Avalonia.Rendering;
struct OwnedDisposable<T> :IDisposable where T : class, IDisposable
{
private readonly bool _owns;
private T? _value;
public T Value => _value ?? throw new ObjectDisposedException("OwnedDisposable");
public OwnedDisposable(T value, bool owns)
{
_owns = owns;
_value = value;
}
public void Dispose()
{
if(_owns)
_value?.Dispose();
_value = null;
}
}

66
src/Avalonia.Base/Rendering/PlatformRenderInterfaceContextManager.cs

@ -0,0 +1,66 @@
using System;
using System.Collections.Generic;
using System.Reactive.Disposables;
using Avalonia.Metadata;
using Avalonia.Platform;
namespace Avalonia.Rendering;
[Unstable]
// TODO: Make it internal once legacy renderers are removed
public class PlatformRenderInterfaceContextManager
{
private readonly IPlatformGraphics? _graphics;
private IPlatformRenderInterfaceContext? _backend;
private OwnedDisposable<IPlatformGraphicsContext>? _gpuContext;
public PlatformRenderInterfaceContextManager(IPlatformGraphics? graphics)
{
_graphics = graphics;
}
public void EnsureValidBackendContext()
{
if (_backend == null || _gpuContext?.Value.IsLost == true)
{
_backend?.Dispose();
_backend = null;
_gpuContext?.Dispose();
_gpuContext = null;
if (_graphics != null)
{
if (_graphics.UsesSharedContext)
_gpuContext = new OwnedDisposable<IPlatformGraphicsContext>(_graphics.GetSharedContext(), false);
else
_gpuContext = new OwnedDisposable<IPlatformGraphicsContext>(_graphics.CreateContext(), true);
}
_backend = AvaloniaLocator.Current.GetRequiredService<IPlatformRenderInterface>()
.CreateBackendContext(_gpuContext?.Value);
}
}
public IPlatformRenderInterfaceContext Value
{
get
{
EnsureValidBackendContext();
return _backend!;
}
}
public IDisposable EnsureCurrent()
{
EnsureValidBackendContext();
if (_gpuContext.HasValue)
return _gpuContext.Value.Value.EnsureCurrent();
return Disposable.Empty;
}
public IRenderTarget CreateRenderTarget(IEnumerable<object> surfaces)
{
EnsureValidBackendContext();
return _backend!.CreateRenderTarget(surfaces);
}
}

26
src/Avalonia.Base/Rendering/RenderLoop.cs

@ -19,6 +19,7 @@ namespace Avalonia.Rendering
private readonly IDispatcher _dispatcher; private readonly IDispatcher _dispatcher;
private List<IRenderLoopTask> _items = new List<IRenderLoopTask>(); private List<IRenderLoopTask> _items = new List<IRenderLoopTask>();
private List<IRenderLoopTask> _itemsCopy = new List<IRenderLoopTask>(); private List<IRenderLoopTask> _itemsCopy = new List<IRenderLoopTask>();
private List<IRenderLoopTask> _updateItemsCopy = new List<IRenderLoopTask>();
private IRenderTimer? _timer; private IRenderTimer? _timer;
private int _inTick; private int _inTick;
private int _inUpdate; private int _inUpdate;
@ -97,7 +98,13 @@ namespace Avalonia.Rendering
{ {
bool needsUpdate = false; bool needsUpdate = false;
foreach (IRenderLoopTask item in _items) lock (_items)
{
_itemsCopy.Clear();
_itemsCopy.AddRange(_items);
}
foreach (IRenderLoopTask item in _itemsCopy)
{ {
if (item.NeedsUpdate) if (item.NeedsUpdate)
{ {
@ -112,10 +119,13 @@ namespace Avalonia.Rendering
{ {
_dispatcher.Post(() => _dispatcher.Post(() =>
{ {
for (var i = 0; i < _items.Count; ++i) lock (_items)
{
_updateItemsCopy.Clear();
_updateItemsCopy.AddRange(_items);
}
foreach (var item in _updateItemsCopy)
{ {
var item = _items[i];
if (item.NeedsUpdate) if (item.NeedsUpdate)
{ {
try try
@ -128,18 +138,12 @@ namespace Avalonia.Rendering
} }
} }
} }
_updateItemsCopy.Clear();
Interlocked.Exchange(ref _inUpdate, 0); Interlocked.Exchange(ref _inUpdate, 0);
}, DispatcherPriority.Render); }, DispatcherPriority.Render);
} }
lock (_items)
{
_itemsCopy.Clear();
foreach (var i in _items)
_itemsCopy.Add(i);
}
for (int i = 0; i < _itemsCopy.Count; i++) for (int i = 0; i < _itemsCopy.Count; i++)
{ {
_itemsCopy[i].Render(); _itemsCopy[i].Render();

2
src/Avalonia.Base/Rendering/SceneGraph/Scene.cs

@ -346,5 +346,7 @@ namespace Avalonia.Rendering.SceneGraph
} }
public void MarkAsRendered() => _rendered.TrySetResult(true); public void MarkAsRendered() => _rendered.TrySetResult(true);
public List<Action>? RenderThreadJobs { get; set; }
} }
} }

67
src/Avalonia.Base/StyledElement.cs

@ -81,6 +81,7 @@ namespace Avalonia
private Styles? _styles; private Styles? _styles;
private bool _stylesApplied; private bool _stylesApplied;
private bool _themeApplied; private bool _themeApplied;
private bool _templatedParentThemeApplied;
private AvaloniaObject? _templatedParent; private AvaloniaObject? _templatedParent;
private bool _dataContextUpdating; private bool _dataContextUpdating;
private ControlTheme? _implicitTheme; private ControlTheme? _implicitTheme;
@ -375,6 +376,12 @@ namespace Avalonia
_themeApplied = true; _themeApplied = true;
} }
if (!_templatedParentThemeApplied)
{
ApplyTemplatedParentControlTheme();
_templatedParentThemeApplied = true;
}
if (!_stylesApplied) if (!_stylesApplied)
{ {
ApplyStyles(this); ApplyStyles(this);
@ -613,26 +620,38 @@ namespace Avalonia
base.OnPropertyChanged(change); base.OnPropertyChanged(change);
if (change.Property == ThemeProperty) if (change.Property == ThemeProperty)
{
OnControlThemeChanged(); OnControlThemeChanged();
_themeApplied = false;
}
} }
private protected virtual void OnControlThemeChanged() private protected virtual void OnControlThemeChanged()
{ {
var values = GetValueStore(); var values = GetValueStore();
values.BeginStyling(); values.BeginStyling();
try { values.RemoveFrames(FrameType.Theme); }
finally { values.EndStyling(); } try
{
values.RemoveFrames(FrameType.Theme);
}
finally
{
values.EndStyling();
_themeApplied = false;
}
} }
internal virtual void OnTemplatedParentControlThemeChanged() internal virtual void OnTemplatedParentControlThemeChanged()
{ {
var values = GetValueStore(); var values = GetValueStore();
values.BeginStyling(); values.BeginStyling();
try { values.RemoveFrames(FrameType.TemplatedParentTheme); } try
finally { values.EndStyling(); } {
values.RemoveFrames(FrameType.TemplatedParentTheme);
}
finally
{
values.EndStyling();
_templatedParentThemeApplied = false;
}
} }
internal ControlTheme? GetEffectiveTheme() internal ControlTheme? GetEffectiveTheme()
@ -743,13 +762,13 @@ namespace Avalonia
private void ApplyControlTheme() private void ApplyControlTheme()
{ {
var theme = GetEffectiveTheme(); if (GetEffectiveTheme() is { } theme)
if (theme is not null)
ApplyControlTheme(theme, FrameType.Theme); ApplyControlTheme(theme, FrameType.Theme);
}
if (TemplatedParent is StyledElement styleableParent && private void ApplyTemplatedParentControlTheme()
styleableParent.GetEffectiveTheme() is { } parentTheme) {
if ((TemplatedParent as StyledElement)?.GetEffectiveTheme() is { } parentTheme)
{ {
ApplyControlTheme(parentTheme, FrameType.TemplatedParentTheme); ApplyControlTheme(parentTheme, FrameType.TemplatedParentTheme);
} }
@ -793,6 +812,28 @@ namespace Avalonia
ApplyStyle(child, host, type); ApplyStyle(child, host, type);
} }
private void ReevaluateImplicitTheme()
{
// We only need to check if the theme has changed when Theme isn't set (i.e. when we
// have an implicit theme).
if (Theme is not null)
return;
// Refetch the implicit theme.
var oldImplicitTheme = _implicitTheme == s_invalidTheme ? null : _implicitTheme;
_implicitTheme = null;
GetEffectiveTheme();
var newImplicitTheme = _implicitTheme == s_invalidTheme ? null : _implicitTheme;
// If the implicit theme has changed, detach the existing theme.
if (newImplicitTheme != oldImplicitTheme)
{
OnControlThemeChanged();
_themeApplied = false;
}
}
private void OnAttachedToLogicalTreeCore(LogicalTreeAttachmentEventArgs e) private void OnAttachedToLogicalTreeCore(LogicalTreeAttachmentEventArgs e)
{ {
if (this.GetLogicalParent() == null && !(this is ILogicalRoot)) if (this.GetLogicalParent() == null && !(this is ILogicalRoot))
@ -811,6 +852,7 @@ namespace Avalonia
{ {
_logicalRoot = e.Root; _logicalRoot = e.Root;
ReevaluateImplicitTheme();
ApplyStyling(); ApplyStyling();
NotifyResourcesChanged(propagate: false); NotifyResourcesChanged(propagate: false);
@ -835,7 +877,6 @@ namespace Avalonia
if (_logicalRoot != null) if (_logicalRoot != null)
{ {
_logicalRoot = null; _logicalRoot = null;
_implicitTheme = null;
InvalidateStyles(recurse: false); InvalidateStyles(recurse: false);
OnDetachedFromLogicalTree(e); OnDetachedFromLogicalTree(e);
DetachedFromLogicalTree?.Invoke(this, e); DetachedFromLogicalTree?.Invoke(this, e);

3
src/Avalonia.Base/Styling/StyleInstance.cs

@ -70,6 +70,9 @@ namespace Avalonia.Styling
_animationTrigger ??= new Subject<bool>(); _animationTrigger ??= new Subject<bool>();
foreach (var animation in _animations) foreach (var animation in _animations)
animation.Apply(animatable, null, _animationTrigger); animation.Apply(animatable, null, _animationTrigger);
if (_activator is null)
_animationTrigger.OnNext(true);
} }
} }

8
src/Avalonia.Base/Utilities/ArraySlice.cs

@ -111,14 +111,6 @@ namespace Avalonia.Utilities
} }
} }
/// <summary>
/// Defines an implicit conversion of a <see cref="ArraySlice{T}"/> to a <see cref="ReadOnlySlice{T}"/>
/// </summary>
public static implicit operator ReadOnlySlice<T>(ArraySlice<T> slice)
{
return new ReadOnlySlice<T>(slice._data, 0, slice.Length, slice.Start);
}
/// <summary> /// <summary>
/// Defines an implicit conversion of an array to a <see cref="ArraySlice{T}"/> /// Defines an implicit conversion of an array to a <see cref="ArraySlice{T}"/>
/// </summary> /// </summary>

239
src/Avalonia.Base/Utilities/ReadOnlySlice.cs

@ -1,239 +0,0 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.Runtime.CompilerServices;
namespace Avalonia.Utilities
{
/// <summary>
/// ReadOnlySlice enables the ability to work with a sequence within a region of memory and retains the position in within that region.
/// </summary>
/// <typeparam name="T">The type of elements in the slice.</typeparam>
[DebuggerTypeProxy(typeof(ReadOnlySlice<>.ReadOnlySliceDebugView))]
public readonly record struct ReadOnlySlice<T> : IReadOnlyList<T> where T : struct
{
private readonly int _bufferOffset;
/// <summary>
/// Gets an empty <see cref="ReadOnlySlice{T}"/>
/// </summary>
public static ReadOnlySlice<T> Empty => new ReadOnlySlice<T>(Array.Empty<T>());
private readonly ReadOnlyMemory<T> _buffer;
public ReadOnlySlice(ReadOnlyMemory<T> buffer) : this(buffer, 0, buffer.Length) { }
public ReadOnlySlice(ReadOnlyMemory<T> buffer, int start, int length, int bufferOffset = 0)
{
#if DEBUG
if (start.CompareTo(0) < 0)
{
throw new ArgumentOutOfRangeException(nameof (start));
}
if (length.CompareTo(buffer.Length) > 0)
{
throw new ArgumentOutOfRangeException(nameof (length));
}
#endif
_buffer = buffer;
Start = start;
Length = length;
_bufferOffset = bufferOffset;
}
/// <summary>
/// Gets the start.
/// </summary>
/// <value>
/// The start.
/// </value>
public int Start { get; }
/// <summary>
/// Gets the end.
/// </summary>
/// <value>
/// The end.
/// </value>
public int End => Start + Length - 1;
/// <summary>
/// Gets the length.
/// </summary>
/// <value>
/// The length.
/// </value>
public int Length { get; }
/// <summary>
/// Gets a value that indicates whether this instance of <see cref="ReadOnlySlice{T}"/> is Empty.
/// </summary>
public bool IsEmpty => Length == 0;
/// <summary>
/// Get the underlying span.
/// </summary>
public ReadOnlySpan<T> Span => _buffer.Span.Slice(_bufferOffset, Length);
/// <summary>
/// Get the buffer offset.
/// </summary>
public int BufferOffset => _bufferOffset;
/// <summary>
/// Get the underlying buffer.
/// </summary>
public ReadOnlyMemory<T> Buffer => _buffer;
/// <summary>
/// Returns a value to specified element of the slice.
/// </summary>
/// <param name="index">The index of the element to return.</param>
/// <returns>The <typeparamref name="T"/>.</returns>
/// <exception cref="IndexOutOfRangeException">
/// Thrown when index less than 0 or index greater than or equal to <see cref="Length"/>.
/// </exception>
public T this[int index]
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get
{
#if DEBUG
if (index.CompareTo(0) < 0 || index.CompareTo(Length) > 0)
{
throw new ArgumentOutOfRangeException(nameof (index));
}
#endif
return Span[index];
}
}
/// <summary>
/// Returns a sub slice of elements that start at the specified index and has the specified number of elements.
/// </summary>
/// <param name="start">The start of the sub slice.</param>
/// <param name="length">The length of the sub slice.</param>
/// <returns>A <see cref="ReadOnlySlice{T}"/> that contains the specified number of elements from the specified start.</returns>
public ReadOnlySlice<T> AsSlice(int start, int length)
{
if (IsEmpty)
{
return this;
}
if (length == 0)
{
return Empty;
}
if (start < 0 || _bufferOffset + start > _buffer.Length - 1)
{
throw new ArgumentOutOfRangeException(nameof(start));
}
if (_bufferOffset + start + length > _buffer.Length)
{
throw new ArgumentOutOfRangeException(nameof(length));
}
return new ReadOnlySlice<T>(_buffer, start, length, _bufferOffset);
}
/// <summary>
/// Returns a specified number of contiguous elements from the start of the slice.
/// </summary>
/// <param name="length">The number of elements to return.</param>
/// <returns>A <see cref="ReadOnlySlice{T}"/> that contains the specified number of elements from the start of this slice.</returns>
public ReadOnlySlice<T> Take(int length)
{
if (IsEmpty)
{
return this;
}
if (length > Length)
{
throw new ArgumentOutOfRangeException(nameof(length));
}
return new ReadOnlySlice<T>(_buffer, Start, length, _bufferOffset);
}
/// <summary>
/// Bypasses a specified number of elements in the slice and then returns the remaining elements.
/// </summary>
/// <param name="length">The number of elements to skip before returning the remaining elements.</param>
/// <returns>A <see cref="ReadOnlySlice{T}"/> that contains the elements that occur after the specified index in this slice.</returns>
public ReadOnlySlice<T> Skip(int length)
{
if (IsEmpty)
{
return this;
}
if (length > Length)
{
throw new ArgumentOutOfRangeException(nameof(length));
}
return new ReadOnlySlice<T>(_buffer, Start + length, Length - length, _bufferOffset + length);
}
/// <summary>
/// Returns an enumerator for the slice.
/// </summary>
public ImmutableReadOnlyListStructEnumerator<T> GetEnumerator()
{
return new ImmutableReadOnlyListStructEnumerator<T>(this);
}
IEnumerator<T> IEnumerable<T>.GetEnumerator()
{
return GetEnumerator();
}
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
int IReadOnlyCollection<T>.Count => Length;
T IReadOnlyList<T>.this[int index] => this[index];
public static implicit operator ReadOnlySlice<T>(T[] array)
{
return new ReadOnlySlice<T>(array);
}
public static implicit operator ReadOnlySlice<T>(ReadOnlyMemory<T> memory)
{
return new ReadOnlySlice<T>(memory);
}
public static implicit operator ReadOnlySpan<T>(ReadOnlySlice<T> slice) => slice.Span;
internal class ReadOnlySliceDebugView
{
private readonly ReadOnlySlice<T> _readOnlySlice;
public ReadOnlySliceDebugView(ReadOnlySlice<T> readOnlySlice)
{
_readOnlySlice = readOnlySlice;
}
public int Start => _readOnlySlice.Start;
public int End => _readOnlySlice.End;
public int Length => _readOnlySlice.Length;
public bool IsEmpty => _readOnlySlice.IsEmpty;
public ReadOnlySpan<T> Items => _readOnlySlice.Span;
}
}
}

90
src/Avalonia.Base/Visual.cs

@ -89,6 +89,14 @@ namespace Avalonia
public static readonly StyledProperty<RelativePoint> RenderTransformOriginProperty = public static readonly StyledProperty<RelativePoint> RenderTransformOriginProperty =
AvaloniaProperty.Register<Visual, RelativePoint>(nameof(RenderTransformOrigin), defaultValue: RelativePoint.Center); AvaloniaProperty.Register<Visual, RelativePoint>(nameof(RenderTransformOrigin), defaultValue: RelativePoint.Center);
/// <summary>
/// Defines the <see cref="FlowDirection"/> property.
/// </summary>
public static readonly AttachedProperty<FlowDirection> FlowDirectionProperty =
AvaloniaProperty.RegisterAttached<Visual, Visual, FlowDirection>(
nameof(FlowDirection),
inherits: true);
/// <summary> /// <summary>
/// Defines the <see cref="VisualParent"/> property. /// Defines the <see cref="VisualParent"/> property.
/// </summary> /// </summary>
@ -263,6 +271,15 @@ namespace Avalonia
set { SetValue(RenderTransformOriginProperty, value); } set { SetValue(RenderTransformOriginProperty, value); }
} }
/// <summary>
/// Gets or sets the text flow direction.
/// </summary>
public FlowDirection FlowDirection
{
get => GetValue(FlowDirectionProperty);
set => SetValue(FlowDirectionProperty, value);
}
/// <summary> /// <summary>
/// Gets or sets the Z index of the control. /// Gets or sets the Z index of the control.
/// </summary> /// </summary>
@ -306,6 +323,36 @@ namespace Avalonia
/// </summary> /// </summary>
internal Visual? VisualParent => _visualParent; internal Visual? VisualParent => _visualParent;
/// <summary>
/// Gets a value indicating whether control bypass FlowDirecton policies.
/// </summary>
/// <remarks>
/// Related to FlowDirection system and returns false as default, so if
/// <see cref="FlowDirection"/> is RTL then control will get a mirror presentation.
/// For controls that want to avoid this behavior, override this property and return true.
/// </remarks>
protected virtual bool BypassFlowDirectionPolicies => false;
/// <summary>
/// Gets the value of the attached <see cref="FlowDirectionProperty"/> on a control.
/// </summary>
/// <param name="visual">The control.</param>
/// <returns>The flow direction.</returns>
public static FlowDirection GetFlowDirection(Visual visual)
{
return visual.GetValue(FlowDirectionProperty);
}
/// <summary>
/// Sets the value of the attached <see cref="FlowDirectionProperty"/> on a control.
/// </summary>
/// <param name="visual">The control.</param>
/// <param name="value">The property value to set.</param>
public static void SetFlowDirection(Visual visual, FlowDirection value)
{
visual.SetValue(FlowDirectionProperty, value);
}
/// <summary> /// <summary>
/// Invalidates the visual and queues a repaint. /// Invalidates the visual and queues a repaint.
/// </summary> /// </summary>
@ -387,6 +434,22 @@ namespace Avalonia
} }
} }
/// <inheritdoc/>
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
{
base.OnPropertyChanged(change);
if (change.Property == FlowDirectionProperty)
{
InvalidateMirrorTransform();
foreach (var child in VisualChildren)
{
child.InvalidateMirrorTransform();
}
}
}
protected override void LogicalChildrenCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e) protected override void LogicalChildrenCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
{ {
base.LogicalChildrenCollectionChanged(sender, e); base.LogicalChildrenCollectionChanged(sender, e);
@ -682,5 +745,32 @@ namespace Avalonia
visual.SetVisualParent(parent); visual.SetVisualParent(parent);
} }
} }
/// <summary>
/// Computes the <see cref="HasMirrorTransform"/> value according to the
/// <see cref="FlowDirection"/> and <see cref="BypassFlowDirectionPolicies"/>
/// </summary>
public virtual void InvalidateMirrorTransform()
{
var flowDirection = this.FlowDirection;
var parentFlowDirection = FlowDirection.LeftToRight;
bool bypassFlowDirectionPolicies = BypassFlowDirectionPolicies;
bool parentBypassFlowDirectionPolicies = false;
var parent = VisualParent;
if (parent != null)
{
parentFlowDirection = parent.FlowDirection;
parentBypassFlowDirectionPolicies = parent.BypassFlowDirectionPolicies;
}
bool thisShouldBeMirrored = flowDirection == FlowDirection.RightToLeft && !bypassFlowDirectionPolicies;
bool parentShouldBeMirrored = parentFlowDirection == FlowDirection.RightToLeft && !parentBypassFlowDirectionPolicies;
bool shouldApplyMirrorTransform = thisShouldBeMirrored != parentShouldBeMirrored;
HasMirrorTransform = shouldApplyMirrorTransform;
}
} }
} }

6
src/Avalonia.Controls.DataGrid/DataGridTemplateColumn.cs

@ -64,9 +64,11 @@ namespace Avalonia.Controls
protected override Control GenerateElement(DataGridCell cell, object dataItem) protected override Control GenerateElement(DataGridCell cell, object dataItem)
{ {
if(CellTemplate != null) if (CellTemplate != null)
{ {
return CellTemplate.Build(dataItem); return (CellTemplate is IRecyclingDataTemplate recyclingDataTemplate)
? recyclingDataTemplate.Build(dataItem, cell.Content as Control)
: CellTemplate.Build(dataItem);
} }
if (Design.IsDesignMode) if (Design.IsDesignMode)
{ {

4
src/Avalonia.Controls.DataGrid/Themes/Fluent.xaml

@ -6,8 +6,8 @@
<StreamGeometry x:Key="DataGridSortIconDescendingPath">M1875 1011l-787 787v-1798h-128v1798l-787 -787l-90 90l941 941l941 -941z</StreamGeometry> <StreamGeometry x:Key="DataGridSortIconDescendingPath">M1875 1011l-787 787v-1798h-128v1798l-787 -787l-90 90l941 941l941 -941z</StreamGeometry>
<StreamGeometry x:Key="DataGridSortIconAscendingPath">M1965 947l-941 -941l-941 941l90 90l787 -787v1798h128v-1798l787 787z</StreamGeometry> <StreamGeometry x:Key="DataGridSortIconAscendingPath">M1965 947l-941 -941l-941 941l90 90l787 -787v1798h128v-1798l787 787z</StreamGeometry>
<StreamGeometry x:Key="DataGridRowGroupHeaderIconClosedPath">M515 93l930 931l-930 931l90 90l1022 -1021l-1022 -1021z</StreamGeometry> <StreamGeometry x:Key="DataGridRowGroupHeaderIconOpenedPath">M515 93l930 931l-930 931l90 90l1022 -1021l-1022 -1021z</StreamGeometry>
<StreamGeometry x:Key="DataGridRowGroupHeaderIconOpenedPath">M109 486 19 576 1024 1581 2029 576 1939 486 1024 1401z</StreamGeometry> <StreamGeometry x:Key="DataGridRowGroupHeaderIconClosedPath">M109 486 19 576 1024 1581 2029 576 1939 486 1024 1401z</StreamGeometry>
<SolidColorBrush x:Key="DataGridColumnHeaderForegroundBrush" Color="{DynamicResource SystemBaseMediumColor}" /> <SolidColorBrush x:Key="DataGridColumnHeaderForegroundBrush" Color="{DynamicResource SystemBaseMediumColor}" />
<SolidColorBrush x:Key="DataGridColumnHeaderBackgroundBrush" Color="{DynamicResource SystemAltHighColor}" /> <SolidColorBrush x:Key="DataGridColumnHeaderBackgroundBrush" Color="{DynamicResource SystemAltHighColor}" />

2
src/Avalonia.Controls/AppBuilderBase.cs

@ -229,8 +229,8 @@ namespace Avalonia.Controls
s_setupWasAlreadyCalled = true; s_setupWasAlreadyCalled = true;
_optionsInitializers?.Invoke(); _optionsInitializers?.Invoke();
RuntimePlatformServicesInitializer(); RuntimePlatformServicesInitializer();
WindowingSubsystemInitializer();
RenderingSubsystemInitializer(); RenderingSubsystemInitializer();
WindowingSubsystemInitializer();
AfterPlatformServicesSetupCallback(Self); AfterPlatformServicesSetupCallback(Self);
Instance = _appFactory(); Instance = _appFactory();
Instance.ApplicationLifetime = _lifetime; Instance.ApplicationLifetime = _lifetime;

5
src/Avalonia.Controls/ComboBox.cs

@ -454,10 +454,9 @@ namespace Avalonia.Controls
{ {
if (SelectionBoxItem is Rectangle rectangle) if (SelectionBoxItem is Rectangle rectangle)
{ {
if ((rectangle.Fill as VisualBrush)?.Visual is Control content) if ((rectangle.Fill as VisualBrush)?.Visual is Visual content)
{ {
var flowDirection = (((Visual)content!).VisualParent as Control)?.FlowDirection ?? var flowDirection = content.VisualParent?.FlowDirection ?? FlowDirection.LeftToRight;
FlowDirection.LeftToRight;
rectangle.FlowDirection = flowDirection; rectangle.FlowDirection = flowDirection;
} }
} }

85
src/Avalonia.Controls/Control.cs

@ -91,13 +91,6 @@ namespace Avalonia.Controls
RoutedEvent.Register<Control, SizeChangedEventArgs>( RoutedEvent.Register<Control, SizeChangedEventArgs>(
nameof(SizeChanged), RoutingStrategies.Direct); nameof(SizeChanged), RoutingStrategies.Direct);
/// <summary>
/// Defines the <see cref="FlowDirection"/> property.
/// </summary>
public static readonly AttachedProperty<FlowDirection> FlowDirectionProperty =
AvaloniaProperty.RegisterAttached<Control, Control, FlowDirection>(
nameof(FlowDirection),
inherits: true);
// Note the following: // Note the following:
// _loadedQueue : // _loadedQueue :
@ -170,15 +163,6 @@ namespace Avalonia.Controls
get => GetValue(TagProperty); get => GetValue(TagProperty);
set => SetValue(TagProperty, value); set => SetValue(TagProperty, value);
} }
/// <summary>
/// Gets or sets the text flow direction.
/// </summary>
public FlowDirection FlowDirection
{
get => GetValue(FlowDirectionProperty);
set => SetValue(FlowDirectionProperty, value);
}
/// <summary> /// <summary>
/// Occurs when the user has completed a context input gesture, such as a right-click. /// Occurs when the user has completed a context input gesture, such as a right-click.
@ -229,39 +213,9 @@ namespace Avalonia.Controls
public new Control? Parent => (Control?)base.Parent; public new Control? Parent => (Control?)base.Parent;
/// <summary>
/// Gets the value of the attached <see cref="FlowDirectionProperty"/> on a control.
/// </summary>
/// <param name="control">The control.</param>
/// <returns>The flow direction.</returns>
public static FlowDirection GetFlowDirection(Control control)
{
return control.GetValue(FlowDirectionProperty);
}
/// <summary>
/// Sets the value of the attached <see cref="FlowDirectionProperty"/> on a control.
/// </summary>
/// <param name="control">The control.</param>
/// <param name="value">The property value to set.</param>
public static void SetFlowDirection(Control control, FlowDirection value)
{
control.SetValue(FlowDirectionProperty, value);
}
/// <inheritdoc/> /// <inheritdoc/>
bool IDataTemplateHost.IsDataTemplatesInitialized => _dataTemplates != null; bool IDataTemplateHost.IsDataTemplatesInitialized => _dataTemplates != null;
/// <summary>
/// Gets a value indicating whether control bypass FlowDirecton policies.
/// </summary>
/// <remarks>
/// Related to FlowDirection system and returns false as default, so if
/// <see cref="FlowDirection"/> is RTL then control will get a mirror presentation.
/// For controls that want to avoid this behavior, override this property and return true.
/// </remarks>
protected virtual bool BypassFlowDirectionPolicies => false;
/// <inheritdoc/> /// <inheritdoc/>
void ISetterValue.Initialize(ISetter setter) void ISetterValue.Initialize(ISetter setter)
{ {
@ -571,45 +525,6 @@ namespace Avalonia.Controls
RaiseEvent(sizeChangedEventArgs); RaiseEvent(sizeChangedEventArgs);
} }
} }
else if (change.Property == FlowDirectionProperty)
{
InvalidateMirrorTransform();
foreach (var visual in VisualChildren)
{
if (visual is Control child)
{
child.InvalidateMirrorTransform();
}
}
}
}
/// <summary>
/// Computes the <see cref="Visual.HasMirrorTransform"/> value according to the
/// <see cref="FlowDirection"/> and <see cref="BypassFlowDirectionPolicies"/>
/// </summary>
public virtual void InvalidateMirrorTransform()
{
var flowDirection = this.FlowDirection;
var parentFlowDirection = FlowDirection.LeftToRight;
bool bypassFlowDirectionPolicies = BypassFlowDirectionPolicies;
bool parentBypassFlowDirectionPolicies = false;
var parent = this.VisualParent as Control;
if (parent != null)
{
parentFlowDirection = parent.FlowDirection;
parentBypassFlowDirectionPolicies = parent.BypassFlowDirectionPolicies;
}
bool thisShouldBeMirrored = flowDirection == FlowDirection.RightToLeft && !bypassFlowDirectionPolicies;
bool parentShouldBeMirrored = parentFlowDirection == FlowDirection.RightToLeft && !parentBypassFlowDirectionPolicies;
bool shouldApplyMirrorTransform = thisShouldBeMirrored != parentShouldBeMirrored;
HasMirrorTransform = shouldApplyMirrorTransform;
} }
} }
} }

4
src/Avalonia.Controls/Documents/LineBreak.cs

@ -4,7 +4,7 @@ using System.Text;
using Avalonia.Media.TextFormatting; using Avalonia.Media.TextFormatting;
using Avalonia.Metadata; using Avalonia.Metadata;
namespace Avalonia.Controls.Documents namespace Avalonia.Controls.Documents
{ {
/// <summary> /// <summary>
/// LineBreak element that forces a line breaking. /// LineBreak element that forces a line breaking.
@ -21,7 +21,7 @@ namespace Avalonia.Controls.Documents
internal override void BuildTextRun(IList<TextRun> textRuns) internal override void BuildTextRun(IList<TextRun> textRuns)
{ {
var text = Environment.NewLine.AsMemory(); var text = Environment.NewLine;
var textRunProperties = CreateTextRunProperties(); var textRunProperties = CreateTextRunProperties();

2
src/Avalonia.Controls/Documents/Run.cs

@ -52,7 +52,7 @@ namespace Avalonia.Controls.Documents
internal override void BuildTextRun(IList<TextRun> textRuns) internal override void BuildTextRun(IList<TextRun> textRuns)
{ {
var text = (Text ?? "").AsMemory(); var text = Text ?? "";
var textRunProperties = CreateTextRunProperties(); var textRunProperties = CreateTextRunProperties();

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

Loading…
Cancel
Save