Browse Source

fix: merge conflict with master

pull/9628/head
Giuseppe Lippolis 3 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\\SampleControls\\ControlSamples.csproj",
"samples\\Sandbox\\Sandbox.csproj",
"samples\\ReactiveUIDemo\\ReactiveUIDemo.csproj",
"src\\Avalonia.Base\\Avalonia.Base.csproj",
"src\\Avalonia.Build.Tasks\\Avalonia.Build.Tasks.csproj",
"src\\Avalonia.Controls.ColorPicker\\Avalonia.Controls.ColorPicker.csproj",

7
Avalonia.sln

@ -232,6 +232,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ControlCatalog.Browser", "s
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ControlCatalog.Browser.Blazor", "samples\ControlCatalog.Browser.Blazor\ControlCatalog.Browser.Blazor.csproj", "{90B08091-9BBD-4362-B712-E9F2CC62B218}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ReactiveUIDemo", "samples\ReactiveUIDemo\ReactiveUIDemo.csproj", "{75C47156-C5D8-44BC-A5A7-E8657C2248D6}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@ -543,6 +545,10 @@ Global
{90B08091-9BBD-4362-B712-E9F2CC62B218}.Debug|Any CPU.Build.0 = Debug|Any CPU
{90B08091-9BBD-4362-B712-E9F2CC62B218}.Release|Any CPU.ActiveCfg = Release|Any CPU
{90B08091-9BBD-4362-B712-E9F2CC62B218}.Release|Any CPU.Build.0 = Release|Any CPU
{75C47156-C5D8-44BC-A5A7-E8657C2248D6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{75C47156-C5D8-44BC-A5A7-E8657C2248D6}.Debug|Any CPU.Build.0 = Debug|Any CPU
{75C47156-C5D8-44BC-A5A7-E8657C2248D6}.Release|Any CPU.ActiveCfg = Release|Any CPU
{75C47156-C5D8-44BC-A5A7-E8657C2248D6}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@ -607,6 +613,7 @@ Global
{47F8530C-F19B-4B1A-B4D6-EB231522AE5D} = {86A3F706-DC3C-43C6-BE1B-B98F5BAAA268}
{15B93A4C-1B46-43F6-B534-7B25B6E99932} = {9B9E3891-2366-4253-A952-D08BCEB71098}
{90B08091-9BBD-4362-B712-E9F2CC62B218} = {9B9E3891-2366-4253-A952-D08BCEB71098}
{75C47156-C5D8-44BC-A5A7-E8657C2248D6} = {9B9E3891-2366-4253-A952-D08BCEB71098}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {87366D66-1391-4D90-8999-95A620AD786A}

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

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

3
samples/ControlCatalog/MainView.xaml

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

27
samples/ControlCatalog/Pages/RefreshContainerPage.axaml

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

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

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

26
samples/ControlCatalog/ViewModels/RefreshContainerViewModel.cs

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

15
samples/IntegrationTestApp/MainWindow.axaml

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

3
samples/IntegrationTestApp/MainWindow.axaml.cs

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

8
samples/IntegrationTestApp/ShowWindowTest.axaml

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

8
samples/ReactiveUIDemo/App.axaml

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

37
samples/ReactiveUIDemo/App.axaml.cs

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

19
samples/ReactiveUIDemo/MainWindow.axaml

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

22
samples/ReactiveUIDemo/MainWindow.axaml.cs

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

28
samples/ReactiveUIDemo/ReactiveUIDemo.csproj

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

11
samples/ReactiveUIDemo/ViewModels/BarViewModel.cs

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

11
samples/ReactiveUIDemo/ViewModels/FooViewModel.cs

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

9
samples/ReactiveUIDemo/ViewModels/MainWindowViewModel.cs

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

21
samples/ReactiveUIDemo/ViewModels/RoutedViewHostPageViewModel.cs

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

16
samples/ReactiveUIDemo/Views/BarView.axaml

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

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

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

16
samples/ReactiveUIDemo/Views/FooView.axaml

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

@ -2,24 +2,30 @@
{
public readonly record struct GlyphRunMetrics
{
public GlyphRunMetrics(double width, double widthIncludingTrailingWhitespace, int trailingWhitespaceLength,
int newlineLength, double height)
public GlyphRunMetrics(double width, double widthIncludingTrailingWhitespace, double height,
int trailingWhitespaceLength, int newLineLength, int firstCluster, int lastCluster)
{
Width = width;
WidthIncludingTrailingWhitespace = widthIncludingTrailingWhitespace;
TrailingWhitespaceLength = trailingWhitespaceLength;
NewlineLength = newlineLength;
Height = height;
TrailingWhitespaceLength = trailingWhitespaceLength;
NewLineLength= newLineLength;
FirstCluster = firstCluster;
LastCluster = lastCluster;
}
public double Width { get; }
public double WidthIncludingTrailingWhitespace { get; }
public double Height { get; }
public int TrailingWhitespaceLength { get; }
public int NewlineLength { get; }
public int NewLineLength { get; }
public double Height { get; }
public int FirstCluster { get; }
public int LastCluster { get; }
}
}

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

@ -6,13 +6,13 @@
/// </summary>
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,
double widthIncludingTrailingWhitespace)
{
HasOverflowed = hasOverflowed;
Height = height;
NewLineLength = newLineLength;
NewlineLength = newlineLength;
Start = start;
TextBaseline = textBaseline;
TrailingWhitespaceLength = trailingWhitespaceLength;
@ -33,7 +33,7 @@
/// <summary>
/// Gets the number of newline characters at the end of a line.
/// </summary>
public int NewLineLength { get; }
public int NewlineLength { get; }
/// <summary>
/// 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>
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;

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

@ -1,5 +1,4 @@
using System.Diagnostics;
using Avalonia.Utilities;
namespace Avalonia.Media.TextFormatting
{
@ -14,12 +13,12 @@ namespace Avalonia.Media.TextFormatting
/// <summary>
/// Gets the text source length.
/// </summary>
public virtual int TextSourceLength => DefaultTextSourceLength;
public virtual int Length => DefaultTextSourceLength;
/// <summary>
/// Gets the text run's text.
/// </summary>
public virtual ReadOnlySlice<char> Text => default;
public virtual CharacterBufferReference CharacterBufferReference => default;
/// <summary>
/// A set of properties shared by every characters in the run
@ -41,9 +40,11 @@ namespace Avalonia.Media.TextFormatting
{
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.Globalization;
using Avalonia.Platform;
using Avalonia.Utilities;
namespace Avalonia.Media.TextFormatting
{
@ -45,9 +43,14 @@ namespace Avalonia.Media.TextFormatting
}
/// <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 Avalonia.Utilities;
namespace Avalonia.Media.TextFormatting
{
@ -15,7 +14,7 @@ namespace Avalonia.Media.TextFormatting
/// <param name="ellipsis">Text used as collapsing symbol.</param>
/// <param name="width">Width in which collapsing is constrained to.</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;
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="textRunProperties">text run properties of ellipsis symbol.</param>
public TextTrailingWordEllipsis(
ReadOnlySlice<char> ellipsis,
string ellipsis,
double width,
TextRunProperties textRunProperties
)

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

@ -2,6 +2,7 @@
// Licensed under the Apache License, Version 2.0.
// Ported from: https://github.com/SixLabors/Fonts/
using System;
using Avalonia.Utilities;
namespace Avalonia.Media.TextFormatting.Unicode
@ -63,7 +64,7 @@ namespace Avalonia.Media.TextFormatting.Unicode
/// Appends text to the bidi data.
/// </summary>
/// <param name="text">The text to process.</param>
public void Append(ReadOnlySlice<char> text)
public void Append(CharacterBufferRange text)
{
_classes.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 Avalonia.Utilities;
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="count">The count of character that were read.</param>
/// <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;
if (index >= text.Length)
if (index >= text.Count)
{
return ReplacementCodepoint;
}
@ -184,7 +183,7 @@ namespace Avalonia.Media.TextFormatting.Unicode
{
hi = code;
if (index + 1 == text.Length)
if (index + 1 == text.Count)
{
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
{
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;
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
{
/// <summary>
/// Represents the smallest unit of a writing system of any given language.
/// </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;
Text = text;
@ -21,6 +21,6 @@ namespace Avalonia.Media.TextFormatting.Unicode
/// <summary>
/// The text that is representing the <see cref="Grapheme"/>.
/// </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.
using System.Collections.Generic;
using System.Runtime.InteropServices;
using Avalonia.Utilities;
namespace Avalonia.Media.TextFormatting.Unicode
{
public ref struct GraphemeEnumerator
{
private ReadOnlySlice<char> _text;
private CharacterBufferRange _text;
public GraphemeEnumerator(ReadOnlySlice<char> text)
public GraphemeEnumerator(CharacterBufferRange text)
{
_text = text;
Current = default;
@ -187,7 +187,7 @@ namespace Avalonia.Media.TextFormatting.Unicode
var text = _text.Take(processor.CurrentCodeUnitOffset);
Current = new Grapheme(firstCodepoint, text);
Current = new Grapheme(firstCodepoint, text.Span);
_text = _text.Skip(processor.CurrentCodeUnitOffset);
@ -197,10 +197,10 @@ namespace Avalonia.Media.TextFormatting.Unicode
[StructLayout(LayoutKind.Auto)]
private ref struct Processor
{
private readonly ReadOnlySlice<char> _buffer;
private readonly CharacterBufferRange _buffer;
private int _codeUnitLengthOfCurrentScalar;
internal Processor(ReadOnlySlice<char> buffer)
internal Processor(CharacterBufferRange buffer)
{
_buffer = buffer;
_codeUnitLengthOfCurrentScalar = 0;

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

@ -2,7 +2,8 @@
// Licensed under the Apache License, Version 2.0.
// Ported from: https://github.com/SixLabors/Fonts/
using Avalonia.Utilities;
using System;
using System.Collections.Generic;
namespace Avalonia.Media.TextFormatting.Unicode
{
@ -12,7 +13,7 @@ namespace Avalonia.Media.TextFormatting.Unicode
/// </summary>
public ref struct LineBreakEnumerator
{
private readonly ReadOnlySlice<char> _text;
private readonly IReadOnlyList<char> _text;
private int _position;
private int _lastPosition;
private LineBreakClass _currentClass;
@ -28,7 +29,7 @@ namespace Avalonia.Media.TextFormatting.Unicode
private int _lb30a;
private bool _lb31;
public LineBreakEnumerator(ReadOnlySlice<char> text)
public LineBreakEnumerator(IReadOnlyList<char> text)
: this()
{
_text = text;
@ -62,7 +63,7 @@ namespace Avalonia.Media.TextFormatting.Unicode
_lb30a = 0;
}
while (_position < _text.Length)
while (_position < _text.Count)
{
_lastPosition = _position;
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;

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

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

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

@ -1,21 +1,16 @@
using Avalonia.Media.TextFormatting;
using Avalonia.Utilities;
namespace Avalonia.Media
{
public sealed class TextTrailingTrimming : TextTrimming
{
private readonly ReadOnlySlice<char> _ellipsis;
private readonly string _ellipsis;
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;
_ellipsis = new ReadOnlySlice<char>(ellipsis);
_ellipsis = ellipsis;
}
public override TextCollapsingProperties CreateCollapsingProperties(TextCollapsingCreateInfo createInfo)

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

@ -8,7 +8,7 @@ namespace Avalonia.Media
/// </summary>
public abstract class TextTrimming
{
internal const char DefaultEllipsisChar = '\u2026';
internal const string DefaultEllipsisChar = "\u2026";
/// <summary>
/// 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;
[Unstable]
public interface IPlatformGpu
public interface IPlatformGraphics
{
IPlatformGpuContext PrimaryContext { get; }
bool UsesSharedContext { get; }
IPlatformGraphicsContext CreateContext();
IPlatformGraphicsContext GetSharedContext();
}
[Unstable]
public interface IPlatformGpuContext : IDisposable
public interface IPlatformGraphicsContext : IDisposable, IOptionalFeatureProvider
{
bool IsLost { get; }
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>
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>
/// Creates a render target bitmap implementation.
/// </summary>
@ -181,6 +172,13 @@ namespace Avalonia.Platform
/// <returns></returns>
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>
/// Gets a value indicating whether the platform directly supports rectangles with rounded corners.
/// </summary>
@ -200,4 +198,17 @@ namespace Avalonia.Platform
/// </summary>
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.
/// </param>
IDrawingContextImpl CreateDrawingContext(IVisualBrushRenderer? visualBrushRenderer);
}
public interface IRenderTargetWithCorruptionInfo : IRenderTarget
{
bool IsCorrupted { get; }
/// <summary>
/// Indicates if the render target is no longer usable and needs to be recreated
/// </summary>
public bool IsCorrupted { get; }
}
}

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

@ -1,6 +1,5 @@
using Avalonia.Media.TextFormatting;
using Avalonia.Metadata;
using Avalonia.Utilities;
namespace Avalonia.Platform
{
@ -13,9 +12,9 @@ namespace Avalonia.Platform
/// <summary>
/// Shapes the specified region within the text and returns a shaped buffer.
/// </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>
/// <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()
{
if (_finished)
{
return;
}
TargetObject.Compositor.AddToClock(this);
base.Activate();
}

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

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

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

@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using Avalonia.Platform;
using Avalonia.Rendering.Composition.Animations;
using Avalonia.Rendering.Composition.Server;
@ -10,11 +11,11 @@ public partial class Compositor
/// <summary>
/// Creates a new CompositionTarget
/// </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>
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));

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

@ -34,6 +34,7 @@ namespace Avalonia.Rendering.Composition
internal ServerCompositor Server => _server;
private Task? _pendingBatch;
private readonly object _pendingBatchLock = new();
private List<Action> _pendingServerCompositorJobs = new();
internal IEasing DefaultEasing { get; }
@ -43,7 +44,7 @@ namespace Avalonia.Rendering.Composition
/// </summary>
/// <param name="loop"></param>
/// <param name="gpu"></param>
public Compositor(IRenderLoop loop, IPlatformGpu? gpu)
public Compositor(IRenderLoop loop, IPlatformGraphics? gpu)
{
Loop = loop;
_server = new ServerCompositor(loop, gpu, _batchObjectPool, _batchMemoryPool);
@ -101,6 +102,13 @@ namespace Avalonia.Rendering.Composition
#endif
}
_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;
@ -136,5 +144,28 @@ namespace Avalonia.Rendering.Composition
_invokeBeforeCommit.Enqueue(action);
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.Diagnostics;
using System.Globalization;
using System.Linq;
using Avalonia.Media;
using Avalonia.Media.TextFormatting;
using Avalonia.Platform;
@ -31,7 +32,7 @@ internal class FpsCounter
{
var s = new string((char)c, 1);
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
{
private readonly ServerCompositor _compositor;
private readonly Func<IRenderTarget> _renderTargetFactory;
private readonly Func<IEnumerable<object>> _surfaces;
private static long s_nextId = 1;
public long Id { get; }
public ulong Revision { get; private set; }
@ -39,11 +39,11 @@ namespace Avalonia.Rendering.Composition.Server
public ReadbackIndices Readback { get; } = new();
public int RenderedVisuals { get; set; }
public ServerCompositionTarget(ServerCompositor compositor, Func<IRenderTarget> renderTargetFactory) :
public ServerCompositionTarget(ServerCompositor compositor, Func<IEnumerable<object>> surfaces) :
base(compositor)
{
_compositor = compositor;
_renderTargetFactory = renderTargetFactory;
_surfaces = surfaces;
Id = Interlocked.Increment(ref s_nextId);
}
@ -79,13 +79,14 @@ namespace Avalonia.Rendering.Composition.Server
if (Root == null)
return;
if ((_renderTarget as IRenderTargetWithCorruptionInfo)?.IsCorrupted == true)
if (_renderTarget?.IsCorrupted == true)
{
_renderTarget!.Dispose();
_renderTarget = null;
_redrawRequested = true;
}
_renderTarget ??= _renderTargetFactory();
_renderTarget ??= _compositor.CreateRenderTarget(_surfaces());
Compositor.UpdateServerTime();
@ -109,12 +110,13 @@ namespace Avalonia.Rendering.Composition.Server
using (var targetContext = _renderTarget.CreateDrawingContext(null))
{
var layerSize = Size * Scaling;
if (layerSize != _layerSize || _layer == null)
if (layerSize != _layerSize || _layer == null || _layer.IsCorrupted)
{
_layer?.Dispose();
_layer = null;
_layer = targetContext.CreateLayer(Size);
_layerSize = layerSize;
_dirtyRect = new Rect(0, 0, layerSize.Width, layerSize.Height);
}
if (!_dirtyRect.IsEmpty)
@ -197,7 +199,7 @@ namespace Avalonia.Rendering.Composition.Server
if(_disposed)
return;
_disposed = true;
using (_compositor.GpuContext?.EnsureCurrent())
using (_compositor.RenderInterface.EnsureCurrent())
{
if (_layer != null)
{

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

@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using Avalonia.Logging;
using Avalonia.Platform;
using Avalonia.Rendering.Composition.Animations;
using Avalonia.Rendering.Composition.Expressions;
@ -19,6 +20,7 @@ namespace Avalonia.Rendering.Composition.Server
internal class ServerCompositor : IRenderLoopTask
{
private readonly IRenderLoop _renderLoop;
private readonly Queue<Batch> _batches = new Queue<Batch>();
public long LastBatchId { get; private set; }
public Stopwatch Clock { get; } = Stopwatch.StartNew();
@ -29,13 +31,15 @@ namespace Avalonia.Rendering.Composition.Server
internal BatchStreamObjectPool<object?> BatchObjectPool;
internal BatchStreamMemoryPool BatchMemoryPool;
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)
{
GpuContext = platformGpu?.PrimaryContext;
_renderLoop = renderLoop;
RenderInterface = new PlatformRenderInterfaceContextManager(platformGraphics);
BatchObjectPool = batchObjectPool;
BatchMemoryPool = batchMemoryPool;
_renderLoop.Add(this);
@ -66,7 +70,14 @@ namespace Avalonia.Rendering.Composition.Server
{
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);
#if DEBUG_COMPOSITOR_SERIALIZATION
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()
{
foreach(var batch in _reusableToCompleteList)
@ -118,8 +146,16 @@ namespace Avalonia.Rendering.Composition.Server
_animationsToUpdate.Clear();
foreach (var t in _activeTargets)
t.Render();
try
{
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)
@ -137,5 +173,11 @@ namespace Avalonia.Rendering.Composition.Server
public void RemoveFromClock(IAnimationInstance 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.IO;
using System.Linq;
using System.Threading.Tasks;
using Avalonia.Logging;
using Avalonia.Media;
using Avalonia.Media.Immutable;
@ -22,6 +23,8 @@ namespace Avalonia.Rendering
{
private readonly IDispatcher? _dispatcher;
private readonly IRenderLoop? _renderLoop;
private readonly Func<IRenderTarget>? _renderTargetFactory;
private readonly PlatformRenderInterfaceContextManager? _renderInterface;
private readonly Visual _root;
private readonly ISceneBuilder _sceneBuilder;
@ -39,6 +42,7 @@ namespace Avalonia.Rendering
private readonly object _startStopLock = new object();
private readonly object _renderLoopIsRenderingLock = new object();
private readonly Action _updateSceneIfNeededDelegate;
private List<Action>? _pendingRenderThreadJobs;
/// <summary>
/// Initializes a new instance of the <see cref="DeferredRenderer"/> class.
@ -51,6 +55,8 @@ namespace Avalonia.Rendering
public DeferredRenderer(
IRenderRoot root,
IRenderLoop renderLoop,
Func<IRenderTarget> renderTargetFactory,
PlatformRenderInterfaceContextManager? renderInterface = null,
ISceneBuilder? sceneBuilder = null,
IDispatcher? dispatcher = null,
IDeferredRendererLock? rendererLock = null) : base(true)
@ -60,6 +66,8 @@ namespace Avalonia.Rendering
_sceneBuilder = sceneBuilder ?? new SceneBuilder();
Layers = new RenderLayers();
_renderLoop = renderLoop;
_renderTargetFactory = renderTargetFactory;
_renderInterface = renderInterface;
_lock = rendererLock ?? new ManagedDeferredRendererLock();
_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 IRenderLoopTask.NeedsUpdate => NeedsUpdate;
@ -337,7 +369,16 @@ namespace Avalonia.Rendering
}
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;
}
if ((RenderTarget as IRenderTargetWithCorruptionInfo)?.IsCorrupted == true)
if (RenderTarget?.IsCorrupted == true)
{
RenderTarget!.Dispose();
RenderTarget = null;
@ -612,7 +653,7 @@ namespace Avalonia.Rendering
if (RenderTarget == null)
{
RenderTarget = ((IRenderRoot)_root).CreateRenderTarget();
RenderTarget = _renderTargetFactory!();
}
context = RenderTarget.CreateDrawingContext(this);
@ -637,7 +678,11 @@ namespace Avalonia.Rendering
}
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;
if (_dirty == null)

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

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

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

@ -1,6 +1,7 @@
using System;
using Avalonia.VisualTree;
using System.Collections.Generic;
using System.Threading.Tasks;
using Avalonia.Rendering.Composition;
namespace Avalonia.Rendering
@ -87,6 +88,11 @@ namespace Avalonia.Rendering
/// Stops the renderer.
/// </summary>
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

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

@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Avalonia.Logging;
using Avalonia.Media;
using Avalonia.Platform;
@ -19,6 +20,8 @@ namespace Avalonia.Rendering
public class ImmediateRenderer : RendererBase, IRenderer, IVisualBrushRenderer
{
private readonly Visual _root;
private readonly Func<IRenderTarget> _renderTargetFactory;
private readonly PlatformRenderInterfaceContextManager? _renderContext;
private readonly IRenderRoot? _renderRoot;
private bool _updateTransformedBounds = true;
private IRenderTarget? _renderTarget;
@ -27,15 +30,19 @@ namespace Avalonia.Rendering
/// Initializes a new instance of the <see cref="ImmediateRenderer"/> class.
/// </summary>
/// <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));
_renderTargetFactory = renderTargetFactory;
_renderContext = renderContext;
_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));
_renderTargetFactory = renderTargetFactory;
_renderRoot = root as IRenderRoot;
_updateTransformedBounds = updateTransformedBounds;
}
@ -54,7 +61,7 @@ namespace Avalonia.Rendering
{
if (_renderTarget == null)
{
_renderTarget = ((IRenderRoot)_root).CreateRenderTarget();
_renderTarget = _renderTargetFactory();
}
try
@ -104,7 +111,7 @@ namespace Avalonia.Rendering
/// <param name="target">The render target.</param>
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)))
{
renderer.Render(context, visual, visual.Bounds);
@ -118,7 +125,9 @@ namespace Avalonia.Rendering
/// <param name="context">The drawing context.</param>
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);
}
@ -185,6 +194,9 @@ namespace Avalonia.Rendering
{
}
public ValueTask<object?> TryGetRenderInterfaceFeature(Type featureType) =>
new(_renderContext?.Value?.TryGetFeature(featureType));
/// <inheritdoc/>
Size IVisualBrushRenderer.GetRenderTargetSize(IVisualBrush brush)
{
@ -201,7 +213,9 @@ namespace Avalonia.Rendering
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);
}

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 List<IRenderLoopTask> _items = new List<IRenderLoopTask>();
private List<IRenderLoopTask> _itemsCopy = new List<IRenderLoopTask>();
private List<IRenderLoopTask> _updateItemsCopy = new List<IRenderLoopTask>();
private IRenderTimer? _timer;
private int _inTick;
private int _inUpdate;
@ -97,7 +98,13 @@ namespace Avalonia.Rendering
{
bool needsUpdate = false;
foreach (IRenderLoopTask item in _items)
lock (_items)
{
_itemsCopy.Clear();
_itemsCopy.AddRange(_items);
}
foreach (IRenderLoopTask item in _itemsCopy)
{
if (item.NeedsUpdate)
{
@ -112,10 +119,13 @@ namespace Avalonia.Rendering
{
_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)
{
try
@ -128,18 +138,12 @@ namespace Avalonia.Rendering
}
}
}
_updateItemsCopy.Clear();
Interlocked.Exchange(ref _inUpdate, 0);
}, DispatcherPriority.Render);
}
lock (_items)
{
_itemsCopy.Clear();
foreach (var i in _items)
_itemsCopy.Add(i);
}
for (int i = 0; i < _itemsCopy.Count; i++)
{
_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 List<Action>? RenderThreadJobs { get; set; }
}
}

67
src/Avalonia.Base/StyledElement.cs

@ -81,6 +81,7 @@ namespace Avalonia
private Styles? _styles;
private bool _stylesApplied;
private bool _themeApplied;
private bool _templatedParentThemeApplied;
private AvaloniaObject? _templatedParent;
private bool _dataContextUpdating;
private ControlTheme? _implicitTheme;
@ -375,6 +376,12 @@ namespace Avalonia
_themeApplied = true;
}
if (!_templatedParentThemeApplied)
{
ApplyTemplatedParentControlTheme();
_templatedParentThemeApplied = true;
}
if (!_stylesApplied)
{
ApplyStyles(this);
@ -613,26 +620,38 @@ namespace Avalonia
base.OnPropertyChanged(change);
if (change.Property == ThemeProperty)
{
OnControlThemeChanged();
_themeApplied = false;
}
}
private protected virtual void OnControlThemeChanged()
{
var values = GetValueStore();
values.BeginStyling();
try { values.RemoveFrames(FrameType.Theme); }
finally { values.EndStyling(); }
try
{
values.RemoveFrames(FrameType.Theme);
}
finally
{
values.EndStyling();
_themeApplied = false;
}
}
internal virtual void OnTemplatedParentControlThemeChanged()
{
var values = GetValueStore();
values.BeginStyling();
try { values.RemoveFrames(FrameType.TemplatedParentTheme); }
finally { values.EndStyling(); }
try
{
values.RemoveFrames(FrameType.TemplatedParentTheme);
}
finally
{
values.EndStyling();
_templatedParentThemeApplied = false;
}
}
internal ControlTheme? GetEffectiveTheme()
@ -743,13 +762,13 @@ namespace Avalonia
private void ApplyControlTheme()
{
var theme = GetEffectiveTheme();
if (theme is not null)
if (GetEffectiveTheme() is { } theme)
ApplyControlTheme(theme, FrameType.Theme);
}
if (TemplatedParent is StyledElement styleableParent &&
styleableParent.GetEffectiveTheme() is { } parentTheme)
private void ApplyTemplatedParentControlTheme()
{
if ((TemplatedParent as StyledElement)?.GetEffectiveTheme() is { } parentTheme)
{
ApplyControlTheme(parentTheme, FrameType.TemplatedParentTheme);
}
@ -793,6 +812,28 @@ namespace Avalonia
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)
{
if (this.GetLogicalParent() == null && !(this is ILogicalRoot))
@ -811,6 +852,7 @@ namespace Avalonia
{
_logicalRoot = e.Root;
ReevaluateImplicitTheme();
ApplyStyling();
NotifyResourcesChanged(propagate: false);
@ -835,7 +877,6 @@ namespace Avalonia
if (_logicalRoot != null)
{
_logicalRoot = null;
_implicitTheme = null;
InvalidateStyles(recurse: false);
OnDetachedFromLogicalTree(e);
DetachedFromLogicalTree?.Invoke(this, e);

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

@ -70,6 +70,9 @@ namespace Avalonia.Styling
_animationTrigger ??= new Subject<bool>();
foreach (var animation in _animations)
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>
/// Defines an implicit conversion of an array to a <see cref="ArraySlice{T}"/>
/// </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 =
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>
/// Defines the <see cref="VisualParent"/> property.
/// </summary>
@ -263,6 +271,15 @@ namespace Avalonia
set { SetValue(RenderTransformOriginProperty, value); }
}
/// <summary>
/// Gets or sets the text flow direction.
/// </summary>
public FlowDirection FlowDirection
{
get => GetValue(FlowDirectionProperty);
set => SetValue(FlowDirectionProperty, value);
}
/// <summary>
/// Gets or sets the Z index of the control.
/// </summary>
@ -306,6 +323,36 @@ namespace Avalonia
/// </summary>
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>
/// Invalidates the visual and queues a repaint.
/// </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)
{
base.LogicalChildrenCollectionChanged(sender, e);
@ -682,5 +745,32 @@ namespace Avalonia
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)
{
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)
{

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="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">M109 486 19 576 1024 1581 2029 576 1939 486 1024 1401z</StreamGeometry>
<StreamGeometry x:Key="DataGridRowGroupHeaderIconOpenedPath">M515 93l930 931l-930 931l90 90l1022 -1021l-1022 -1021z</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="DataGridColumnHeaderBackgroundBrush" Color="{DynamicResource SystemAltHighColor}" />

2
src/Avalonia.Controls/AppBuilderBase.cs

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

5
src/Avalonia.Controls/ComboBox.cs

@ -454,10 +454,9 @@ namespace Avalonia.Controls
{
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 ??
FlowDirection.LeftToRight;
var flowDirection = content.VisualParent?.FlowDirection ?? FlowDirection.LeftToRight;
rectangle.FlowDirection = flowDirection;
}
}

85
src/Avalonia.Controls/Control.cs

@ -91,13 +91,6 @@ namespace Avalonia.Controls
RoutedEvent.Register<Control, SizeChangedEventArgs>(
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:
// _loadedQueue :
@ -170,15 +163,6 @@ namespace Avalonia.Controls
get => GetValue(TagProperty);
set => SetValue(TagProperty, value);
}
/// <summary>
/// Gets or sets the text flow direction.
/// </summary>
public FlowDirection FlowDirection
{
get => GetValue(FlowDirectionProperty);
set => SetValue(FlowDirectionProperty, value);
}
/// <summary>
/// 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;
/// <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/>
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/>
void ISetterValue.Initialize(ISetter setter)
{
@ -571,45 +525,6 @@ namespace Avalonia.Controls
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.Metadata;
namespace Avalonia.Controls.Documents
namespace Avalonia.Controls.Documents
{
/// <summary>
/// LineBreak element that forces a line breaking.
@ -21,7 +21,7 @@ namespace Avalonia.Controls.Documents
internal override void BuildTextRun(IList<TextRun> textRuns)
{
var text = Environment.NewLine.AsMemory();
var text = Environment.NewLine;
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)
{
var text = (Text ?? "").AsMemory();
var text = Text ?? "";
var textRunProperties = CreateTextRunProperties();

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

Loading…
Cancel
Save