Browse Source

Merge branch 'master' into fix-datagrid-grouping-arrows

pull/9638/head
Max Katz 3 years ago
committed by GitHub
parent
commit
ece5d5daf0
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      .editorconfig
  2. 5
      .ncrunch/ReactiveUIDemo.v3.ncrunchproject
  3. 1
      Avalonia.Desktop.slnf
  4. 7
      Avalonia.sln
  5. 9
      native/Avalonia.Native/src/OSX/WindowImpl.mm
  6. 3
      samples/ControlCatalog/MainView.xaml
  7. 27
      samples/ControlCatalog/Pages/RefreshContainerPage.axaml
  8. 36
      samples/ControlCatalog/Pages/RefreshContainerPage.axaml.cs
  9. 26
      samples/ControlCatalog/ViewModels/RefreshContainerViewModel.cs
  10. 15
      samples/IntegrationTestApp/MainWindow.axaml
  11. 3
      samples/IntegrationTestApp/MainWindow.axaml.cs
  12. 8
      samples/IntegrationTestApp/ShowWindowTest.axaml
  13. 8
      samples/ReactiveUIDemo/App.axaml
  14. 37
      samples/ReactiveUIDemo/App.axaml.cs
  15. 19
      samples/ReactiveUIDemo/MainWindow.axaml
  16. 22
      samples/ReactiveUIDemo/MainWindow.axaml.cs
  17. 28
      samples/ReactiveUIDemo/ReactiveUIDemo.csproj
  18. 11
      samples/ReactiveUIDemo/ViewModels/BarViewModel.cs
  19. 11
      samples/ReactiveUIDemo/ViewModels/FooViewModel.cs
  20. 9
      samples/ReactiveUIDemo/ViewModels/MainWindowViewModel.cs
  21. 21
      samples/ReactiveUIDemo/ViewModels/RoutedViewHostPageViewModel.cs
  22. 16
      samples/ReactiveUIDemo/Views/BarView.axaml
  23. 28
      samples/ReactiveUIDemo/Views/BarView.axaml.cs
  24. 16
      samples/ReactiveUIDemo/Views/FooView.axaml
  25. 28
      samples/ReactiveUIDemo/Views/FooView.axaml.cs
  26. 2
      samples/RenderDemo/Pages/TextFormatterPage.axaml.cs
  27. 2
      src/Android/Avalonia.Android/AndroidInputMethod.cs
  28. 9
      src/Android/Avalonia.Android/AndroidPlatform.cs
  29. 32
      src/Android/Avalonia.Android/OpenGL/GlPlatformSurface.cs
  30. 30
      src/Android/Avalonia.Android/OpenGL/GlRenderTarget.cs
  31. 18
      src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs
  32. 3
      src/Avalonia.Base/Animation/Animators/Animator`1.cs
  33. 11
      src/Avalonia.Base/Animation/Cue.cs
  34. 2
      src/Avalonia.Base/Data/BindingValue.cs
  35. 152
      src/Avalonia.Base/Input/GestureRecognizers/PullGestureRecognizer.cs
  36. 8
      src/Avalonia.Base/Input/Gestures.cs
  37. 5
      src/Avalonia.Base/Input/InputElement.cs
  38. 4
      src/Avalonia.Base/Input/PointerPoint.cs
  39. 43
      src/Avalonia.Base/Input/PullGestureEventArgs.cs
  40. 2
      src/Avalonia.Base/Input/Raw/RawPointerEventArgs.cs
  41. 2
      src/Avalonia.Base/Input/TextInput/ITextInputMethodClient.cs
  42. 2
      src/Avalonia.Base/Logging/ParametrizedLogger.cs
  43. 2
      src/Avalonia.Base/Matrix.cs
  44. 6
      src/Avalonia.Base/Media/BoxShadow.cs
  45. 8
      src/Avalonia.Base/Media/BoxShadows.cs
  46. 2
      src/Avalonia.Base/Media/DrawingContext.cs
  47. 2
      src/Avalonia.Base/Media/FontMetrics.cs
  48. 8
      src/Avalonia.Base/Media/FormattedText.cs
  49. 2
      src/Avalonia.Base/Media/GlyphMetrics.cs
  50. 419
      src/Avalonia.Base/Media/GlyphRun.cs
  51. 20
      src/Avalonia.Base/Media/GlyphRunMetrics.cs
  52. 2
      src/Avalonia.Base/Media/Imaging/RenderTargetBitmap.cs
  53. 2
      src/Avalonia.Base/Media/TextCollapsingCreateInfo.cs
  54. 293
      src/Avalonia.Base/Media/TextFormatting/CharacterBufferRange.cs
  55. 115
      src/Avalonia.Base/Media/TextFormatting/CharacterBufferReference.cs
  56. 13
      src/Avalonia.Base/Media/TextFormatting/FormattedTextSource.cs
  57. 19
      src/Avalonia.Base/Media/TextFormatting/InterWordJustification.cs
  58. 20
      src/Avalonia.Base/Media/TextFormatting/ShapeableTextCharacters.cs
  59. 33
      src/Avalonia.Base/Media/TextFormatting/ShapedBuffer.cs
  60. 19
      src/Avalonia.Base/Media/TextFormatting/ShapedTextCharacters.cs
  61. 4
      src/Avalonia.Base/Media/TextFormatting/SplitResult.cs
  62. 129
      src/Avalonia.Base/Media/TextFormatting/TextCharacters.cs
  63. 102
      src/Avalonia.Base/Media/TextFormatting/TextEllipsisHelper.cs
  64. 4
      src/Avalonia.Base/Media/TextFormatting/TextEndOfLine.cs
  65. 100
      src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs
  66. 2
      src/Avalonia.Base/Media/TextFormatting/TextLayout.cs
  67. 5
      src/Avalonia.Base/Media/TextFormatting/TextLeadingPrefixCharacterEllipsis.cs
  68. 358
      src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs
  69. 8
      src/Avalonia.Base/Media/TextFormatting/TextLineMetrics.cs
  70. 6
      src/Avalonia.Base/Media/TextFormatting/TextMetrics.cs
  71. 2
      src/Avalonia.Base/Media/TextFormatting/TextRange.cs
  72. 11
      src/Avalonia.Base/Media/TextFormatting/TextRun.cs
  73. 2
      src/Avalonia.Base/Media/TextFormatting/TextRunBounds.cs
  74. 11
      src/Avalonia.Base/Media/TextFormatting/TextShaper.cs
  75. 2
      src/Avalonia.Base/Media/TextFormatting/TextShaperOptions.cs
  76. 3
      src/Avalonia.Base/Media/TextFormatting/TextTrailingCharacterEllipsis.cs
  77. 2
      src/Avalonia.Base/Media/TextFormatting/TextTrailingWordEllipsis.cs
  78. 3
      src/Avalonia.Base/Media/TextFormatting/Unicode/BiDiData.cs
  79. 11
      src/Avalonia.Base/Media/TextFormatting/Unicode/Codepoint.cs
  80. 7
      src/Avalonia.Base/Media/TextFormatting/Unicode/CodepointEnumerator.cs
  81. 8
      src/Avalonia.Base/Media/TextFormatting/Unicode/Grapheme.cs
  82. 12
      src/Avalonia.Base/Media/TextFormatting/Unicode/GraphemeEnumerator.cs
  83. 2
      src/Avalonia.Base/Media/TextFormatting/Unicode/LineBreak.cs
  84. 15
      src/Avalonia.Base/Media/TextFormatting/Unicode/LineBreakEnumerator.cs
  85. 2
      src/Avalonia.Base/Media/TextHitTestResult.cs
  86. 11
      src/Avalonia.Base/Media/TextLeadingPrefixTrimming.cs
  87. 11
      src/Avalonia.Base/Media/TextTrailingTrimming.cs
  88. 2
      src/Avalonia.Base/Media/TextTrimming.cs
  89. 12
      src/Avalonia.Base/Media/Transformation/TransformOperation.cs
  90. 2
      src/Avalonia.Base/Media/Transformation/TransformOperations.cs
  91. 4
      src/Avalonia.Base/Media/UnicodeRange.cs
  92. 18
      src/Avalonia.Base/Platform/IOptionalFeatureProvider.cs
  93. 14
      src/Avalonia.Base/Platform/IPlatformGpu.cs
  94. 29
      src/Avalonia.Base/Platform/IPlatformRenderInterface.cs
  95. 10
      src/Avalonia.Base/Platform/IRenderTarget.cs
  96. 2
      src/Avalonia.Base/Platform/IRuntimePlatform.cs
  97. 5
      src/Avalonia.Base/Platform/ITextShaperImpl.cs
  98. 4
      src/Avalonia.Base/Rendering/Composition/Animations/KeyFrameAnimationInstance.cs
  99. 9
      src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs
  100. 7
      src/Avalonia.Base/Rendering/Composition/Compositor.Factories.cs

2
.editorconfig

@ -144,6 +144,8 @@ dotnet_diagnostic.CS1591.severity = suggestion
dotnet_diagnostic.CA1304.severity = warning
# CA1802: Use literals where appropriate
dotnet_diagnostic.CA1802.severity = warning
# CA1815: Override equals and operator equals on value types
dotnet_diagnostic.CA1815.severity = warning
# CA1820: Test for empty strings using string length
dotnet_diagnostic.CA1820.severity = warning
# CA1821: Remove empty finalizers

5
.ncrunch/ReactiveUIDemo.v3.ncrunchproject

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

1
Avalonia.Desktop.slnf

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

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

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

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

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

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

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)

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

2
src/Avalonia.Base/Matrix.cs

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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;

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

@ -4,15 +4,15 @@
/// Represents a metric for a <see cref="TextLine"/> objects,
/// that holds information about ascent, descent, line gap, size and origin of the text line.
/// </summary>
public readonly struct TextLineMetrics
public readonly record struct TextLineMetrics
{
public TextLineMetrics(bool hasOverflowed, double height, int newLineLength, double start, double textBaseline,
public TextLineMetrics(bool hasOverflowed, double height, int newlineLength, double start, double textBaseline,
int trailingWhitespaceLength, double width,
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.

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

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

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

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

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

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

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

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

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

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

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

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

@ -1,10 +1,9 @@
using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using Avalonia.Utilities;
namespace Avalonia.Media.TextFormatting.Unicode
{
public readonly struct Codepoint
public readonly record struct Codepoint
{
private readonly uint _value;
@ -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 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;

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

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

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;

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

@ -5,7 +5,7 @@ namespace Avalonia.Media
/// <summary>
/// Holds a hit test result from a <see cref="TextLayout"/>.
/// </summary>
public readonly struct TextHitTestResult
public readonly record struct TextHitTestResult
{
public TextHitTestResult(CharacterHit characterHit, int textPosition, bool isInside, bool isTrailing)
{

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.

12
src/Avalonia.Base/Media/Transformation/TransformOperation.cs

@ -5,7 +5,7 @@ namespace Avalonia.Media.Transformation
/// <summary>
/// Represents a single primitive transform (like translation, rotation, scale, etc.).
/// </summary>
public struct TransformOperation
public record struct TransformOperation
{
public OperationType Type;
public Matrix Matrix;
@ -196,7 +196,7 @@ namespace Avalonia.Media.Transformation
}
[StructLayout(LayoutKind.Explicit)]
public struct DataLayout
public record struct DataLayout
{
[FieldOffset(0)] public SkewLayout Skew;
@ -206,25 +206,25 @@ namespace Avalonia.Media.Transformation
[FieldOffset(0)] public RotateLayout Rotate;
public struct SkewLayout
public record struct SkewLayout
{
public double X;
public double Y;
}
public struct ScaleLayout
public record struct ScaleLayout
{
public double X;
public double Y;
}
public struct TranslateLayout
public record struct TranslateLayout
{
public double X;
public double Y;
}
public struct RotateLayout
public record struct RotateLayout
{
public double Angle;
}

2
src/Avalonia.Base/Media/Transformation/TransformOperations.cs

@ -165,7 +165,7 @@ namespace Avalonia.Media.Transformation
return Math.Max(from._operations.Count, to._operations.Count);
}
public readonly struct Builder
public readonly record struct Builder
{
private readonly List<TransformOperation> _operations;

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

@ -7,7 +7,7 @@ namespace Avalonia.Media
/// <summary>
/// The <see cref="UnicodeRange"/> descripes a set of Unicode characters.
/// </summary>
public readonly struct UnicodeRange
public readonly record struct UnicodeRange
{
public readonly static UnicodeRange Default = Parse("0-10FFFD");
@ -102,7 +102,7 @@ namespace Avalonia.Media
}
}
public readonly struct UnicodeRangeSegment
public readonly record struct UnicodeRangeSegment
{
private static Regex s_regex = new Regex(@"^(?:[uU]\+)?(?:([0-9a-fA-F](?:[0-9a-fA-F?]{1,5})?))$");

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

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

@ -21,7 +21,7 @@ namespace Avalonia.Platform
}
[Unstable]
public struct RuntimePlatformInfo
public record struct RuntimePlatformInfo
{
public OperatingSystemType OperatingSystem { get; set; }

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

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

Loading…
Cancel
Save