diff --git a/.editorconfig b/.editorconfig index 238e9887bd..ff7ac5d69e 100644 --- a/.editorconfig +++ b/.editorconfig @@ -140,16 +140,22 @@ dotnet_analyzer_diagnostic.category-Performance.severity = none #error - Uncomme # CS1591: Missing XML comment for publicly visible type or member dotnet_diagnostic.CS1591.severity = suggestion +# CS0162: Remove unreachable code +dotnet_diagnostic.CS0162.severity = error # CA1304: Specify CultureInfo 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 dotnet_diagnostic.CA1821.severity = warning # CA1822: Mark members as static dotnet_diagnostic.CA1822.severity = suggestion +# CA1823: Avoid unused private fields +dotnet_diagnostic.CA1823.severity = warning dotnet_code_quality.CA1822.api_surface = private, internal # CA1825: Avoid zero-length array allocations dotnet_diagnostic.CA1825.severity = warning diff --git a/.ncrunch/ReactiveUIDemo.v3.ncrunchproject b/.ncrunch/ReactiveUIDemo.v3.ncrunchproject new file mode 100644 index 0000000000..319cd523ce --- /dev/null +++ b/.ncrunch/ReactiveUIDemo.v3.ncrunchproject @@ -0,0 +1,5 @@ + + + True + + \ No newline at end of file diff --git a/Avalonia.Desktop.slnf b/Avalonia.Desktop.slnf index 6ba05332be..3fa8e969c8 100644 --- a/Avalonia.Desktop.slnf +++ b/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", diff --git a/Avalonia.sln b/Avalonia.sln index bd83cde620..e6898131b0 100644 --- a/Avalonia.sln +++ b/Avalonia.sln @@ -136,8 +136,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Linux", "Linux", "{86C53C40 EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.LinuxFramebuffer", "src\Linux\Avalonia.LinuxFramebuffer\Avalonia.LinuxFramebuffer.csproj", "{854568D5-13D1-4B4F-B50D-534DC7EFD3C9}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Direct3DInteropSample", "samples\interop\Direct3DInteropSample\Direct3DInteropSample.csproj", "{638580B0-7910-40EF-B674-DCB34DA308CD}" -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Win32.Interop", "src\Windows\Avalonia.Win32.Interop\Avalonia.Win32.Interop.csproj", "{CBC4FF2F-92D4-420B-BE21-9FE0B930B04E}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Skia.RenderTests", "tests\Avalonia.Skia.RenderTests\Avalonia.Skia.RenderTests.csproj", "{E1582370-37B3-403C-917F-8209551B1634}" @@ -228,9 +226,11 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Browser", "src\Bro EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Browser.Blazor", "src\Browser\Avalonia.Browser.Blazor\Avalonia.Browser.Blazor.csproj", "{47F8530C-F19B-4B1A-B4D6-EB231522AE5D}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ControlCatalog.Browser", "samples\ControlCatalog.Browser\ControlCatalog.Browser.csproj", "{15B93A4C-1B46-43F6-B534-7B25B6E99932}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ControlCatalog.Browser", "samples\ControlCatalog.Browser\ControlCatalog.Browser.csproj", "{15B93A4C-1B46-43F6-B534-7B25B6E99932}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ControlCatalog.Browser.Blazor", "samples\ControlCatalog.Browser.Blazor\ControlCatalog.Browser.Blazor.csproj", "{90B08091-9BBD-4362-B712-E9F2CC62B218}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ControlCatalog.Browser.Blazor", "samples\ControlCatalog.Browser.Blazor\ControlCatalog.Browser.Blazor.csproj", "{90B08091-9BBD-4362-B712-E9F2CC62B218}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ReactiveUIDemo", "samples\ReactiveUIDemo\ReactiveUIDemo.csproj", "{75C47156-C5D8-44BC-A5A7-E8657C2248D6}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -364,10 +364,6 @@ Global {854568D5-13D1-4B4F-B50D-534DC7EFD3C9}.Debug|Any CPU.Build.0 = Debug|Any CPU {854568D5-13D1-4B4F-B50D-534DC7EFD3C9}.Release|Any CPU.ActiveCfg = Release|Any CPU {854568D5-13D1-4B4F-B50D-534DC7EFD3C9}.Release|Any CPU.Build.0 = Release|Any CPU - {638580B0-7910-40EF-B674-DCB34DA308CD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {638580B0-7910-40EF-B674-DCB34DA308CD}.Debug|Any CPU.Build.0 = Debug|Any CPU - {638580B0-7910-40EF-B674-DCB34DA308CD}.Release|Any CPU.ActiveCfg = Release|Any CPU - {638580B0-7910-40EF-B674-DCB34DA308CD}.Release|Any CPU.Build.0 = Release|Any CPU {CBC4FF2F-92D4-420B-BE21-9FE0B930B04E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {CBC4FF2F-92D4-420B-BE21-9FE0B930B04E}.Debug|Any CPU.Build.0 = Debug|Any CPU {CBC4FF2F-92D4-420B-BE21-9FE0B930B04E}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -543,6 +539,10 @@ Global {90B08091-9BBD-4362-B712-E9F2CC62B218}.Debug|Any CPU.Build.0 = Debug|Any CPU {90B08091-9BBD-4362-B712-E9F2CC62B218}.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 @@ -574,7 +574,6 @@ Global {7D2D3083-71DD-4CC9-8907-39A0D86FB322} = {3743B0F2-CC41-4F14-A8C8-267F579BF91E} {39D7B147-1A5B-47C2-9D01-21FB7C47C4B3} = {9B9E3891-2366-4253-A952-D08BCEB71098} {854568D5-13D1-4B4F-B50D-534DC7EFD3C9} = {86C53C40-57AA-45B8-AD42-FAE0EFDF0F2B} - {638580B0-7910-40EF-B674-DCB34DA308CD} = {A0CC0258-D18C-4AB3-854F-7101680FC3F9} {CBC4FF2F-92D4-420B-BE21-9FE0B930B04E} = {B39A8919-9F95-48FE-AD7B-76E08B509888} {E1582370-37B3-403C-917F-8209551B1634} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B} {E2999E4A-9086-401F-898C-AEB0AD38E676} = {9B9E3891-2366-4253-A952-D08BCEB71098} @@ -607,6 +606,7 @@ Global {47F8530C-F19B-4B1A-B4D6-EB231522AE5D} = {86A3F706-DC3C-43C6-BE1B-B98F5BAAA268} {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} diff --git a/samples/ControlCatalog/MainView.xaml b/samples/ControlCatalog/MainView.xaml index b95b455ca4..4a5f5bc96c 100644 --- a/samples/ControlCatalog/MainView.xaml +++ b/samples/ControlCatalog/MainView.xaml @@ -135,6 +135,9 @@ + + + diff --git a/samples/ControlCatalog/Pages/ImagePage.xaml.cs b/samples/ControlCatalog/Pages/ImagePage.xaml.cs index 511b01c7ac..5b3169a1b0 100644 --- a/samples/ControlCatalog/Pages/ImagePage.xaml.cs +++ b/samples/ControlCatalog/Pages/ImagePage.xaml.cs @@ -70,7 +70,7 @@ namespace ControlCatalog.Pages 3 => new PixelRect(new PixelPoint(bitmapWidth - cropSize.Width, 0), cropSize), 4 => new PixelRect(new PixelPoint(0, bitmapHeight - cropSize.Height), cropSize), 5 => new PixelRect(new PixelPoint(bitmapWidth - cropSize.Width, bitmapHeight - cropSize.Height), cropSize), - _ => PixelRect.Empty + _ => default }; } diff --git a/samples/ControlCatalog/Pages/RefreshContainerPage.axaml b/samples/ControlCatalog/Pages/RefreshContainerPage.axaml new file mode 100644 index 0000000000..f3bf1724b4 --- /dev/null +++ b/samples/ControlCatalog/Pages/RefreshContainerPage.axaml @@ -0,0 +1,27 @@ + + + + + + + + diff --git a/samples/ControlCatalog/Pages/RefreshContainerPage.axaml.cs b/samples/ControlCatalog/Pages/RefreshContainerPage.axaml.cs new file mode 100644 index 0000000000..f9d0328d9a --- /dev/null +++ b/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); + } + } +} diff --git a/samples/ControlCatalog/ViewModels/RefreshContainerViewModel.cs b/samples/ControlCatalog/ViewModels/RefreshContainerViewModel.cs new file mode 100644 index 0000000000..d4b43043be --- /dev/null +++ b/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 Items { get; } + + public RefreshContainerViewModel() + { + Items = new ObservableCollection(Enumerable.Range(1, 200).Select(i => $"Item {i}")); + } + + public async Task AddToTop() + { + await Task.Delay(3000); + Items.Insert(0, $"Item {200 - Items.Count}"); + } + } +} diff --git a/samples/ReactiveUIDemo/App.axaml b/samples/ReactiveUIDemo/App.axaml new file mode 100644 index 0000000000..dd3a39f6ac --- /dev/null +++ b/samples/ReactiveUIDemo/App.axaml @@ -0,0 +1,8 @@ + + + + + diff --git a/samples/ReactiveUIDemo/App.axaml.cs b/samples/ReactiveUIDemo/App.axaml.cs new file mode 100644 index 0000000000..4578566427 --- /dev/null +++ b/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)); + Locator.CurrentMutable.Register(() => new BarView(), typeof(IViewFor)); + } + + 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() + .UsePlatformDetect() + .UseReactiveUI() + .LogToTrace(); + } +} diff --git a/samples/ReactiveUIDemo/MainWindow.axaml b/samples/ReactiveUIDemo/MainWindow.axaml new file mode 100644 index 0000000000..7775fc5a79 --- /dev/null +++ b/samples/ReactiveUIDemo/MainWindow.axaml @@ -0,0 +1,19 @@ + + + + + + + + + + + + + diff --git a/samples/ReactiveUIDemo/MainWindow.axaml.cs b/samples/ReactiveUIDemo/MainWindow.axaml.cs new file mode 100644 index 0000000000..5bf2d476fd --- /dev/null +++ b/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); + } + } +} diff --git a/samples/ReactiveUIDemo/ReactiveUIDemo.csproj b/samples/ReactiveUIDemo/ReactiveUIDemo.csproj new file mode 100644 index 0000000000..94ca4ee809 --- /dev/null +++ b/samples/ReactiveUIDemo/ReactiveUIDemo.csproj @@ -0,0 +1,28 @@ + + + Exe + net6.0 + enable + + + + + + + + + + + BarView.axaml + + + FooView.axaml + + + + + + + + + diff --git a/samples/ReactiveUIDemo/ViewModels/BarViewModel.cs b/samples/ReactiveUIDemo/ViewModels/BarViewModel.cs new file mode 100644 index 0000000000..3448453d81 --- /dev/null +++ b/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; } + } +} diff --git a/samples/ReactiveUIDemo/ViewModels/FooViewModel.cs b/samples/ReactiveUIDemo/ViewModels/FooViewModel.cs new file mode 100644 index 0000000000..1a363e18dc --- /dev/null +++ b/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; } + } +} diff --git a/samples/ReactiveUIDemo/ViewModels/MainWindowViewModel.cs b/samples/ReactiveUIDemo/ViewModels/MainWindowViewModel.cs new file mode 100644 index 0000000000..2222137d38 --- /dev/null +++ b/samples/ReactiveUIDemo/ViewModels/MainWindowViewModel.cs @@ -0,0 +1,9 @@ +using ReactiveUI; + +namespace ReactiveUIDemo.ViewModels +{ + internal class MainWindowViewModel : ReactiveObject + { + public RoutedViewHostPageViewModel RoutedViewHost { get; } = new(); + } +} diff --git a/samples/ReactiveUIDemo/ViewModels/RoutedViewHostPageViewModel.cs b/samples/ReactiveUIDemo/ViewModels/RoutedViewHostPageViewModel.cs new file mode 100644 index 0000000000..701447cfe8 --- /dev/null +++ b/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); + } +} diff --git a/samples/ReactiveUIDemo/Views/BarView.axaml b/samples/ReactiveUIDemo/Views/BarView.axaml new file mode 100644 index 0000000000..2622245997 --- /dev/null +++ b/samples/ReactiveUIDemo/Views/BarView.axaml @@ -0,0 +1,16 @@ + + + + Bar! + + + diff --git a/samples/ReactiveUIDemo/Views/BarView.axaml.cs b/samples/ReactiveUIDemo/Views/BarView.axaml.cs new file mode 100644 index 0000000000..2fbea6de91 --- /dev/null +++ b/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 + { + public BarView() + { + InitializeComponent(); + } + + public BarViewModel? ViewModel { get; set; } + + object? IViewFor.ViewModel + { + get => ViewModel; + set => ViewModel = (BarViewModel?)value; + } + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } + } +} diff --git a/samples/ReactiveUIDemo/Views/FooView.axaml b/samples/ReactiveUIDemo/Views/FooView.axaml new file mode 100644 index 0000000000..8f73250d3b --- /dev/null +++ b/samples/ReactiveUIDemo/Views/FooView.axaml @@ -0,0 +1,16 @@ + + + + Foo! + + + diff --git a/samples/ReactiveUIDemo/Views/FooView.axaml.cs b/samples/ReactiveUIDemo/Views/FooView.axaml.cs new file mode 100644 index 0000000000..313a71044c --- /dev/null +++ b/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 + { + public FooView() + { + InitializeComponent(); + } + + public FooViewModel? ViewModel { get; set; } + + object? IViewFor.ViewModel + { + get => ViewModel; + set => ViewModel = (FooViewModel?)value; + } + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } + } +} diff --git a/samples/RenderDemo/Pages/TextFormatterPage.axaml.cs b/samples/RenderDemo/Pages/TextFormatterPage.axaml.cs index 57a5c7101f..8fbfa854b1 100644 --- a/samples/RenderDemo/Pages/TextFormatterPage.axaml.cs +++ b/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); } } diff --git a/samples/interop/Direct3DInteropSample/App.paml b/samples/interop/Direct3DInteropSample/App.paml deleted file mode 100644 index e6d77dfaf4..0000000000 --- a/samples/interop/Direct3DInteropSample/App.paml +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/samples/interop/Direct3DInteropSample/App.paml.cs b/samples/interop/Direct3DInteropSample/App.paml.cs deleted file mode 100644 index 29365decfe..0000000000 --- a/samples/interop/Direct3DInteropSample/App.paml.cs +++ /dev/null @@ -1,21 +0,0 @@ -using Avalonia; -using Avalonia.Controls.ApplicationLifetimes; -using Avalonia.Markup.Xaml; - -namespace Direct3DInteropSample -{ - public class App : Application - { - public override void Initialize() - { - AvaloniaXamlLoader.Load(this); - } - - public override void OnFrameworkInitializationCompleted() - { - if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) - desktop.MainWindow = new MainWindow(); - base.OnFrameworkInitializationCompleted(); - } - } -} diff --git a/samples/interop/Direct3DInteropSample/Direct3DInteropSample.csproj b/samples/interop/Direct3DInteropSample/Direct3DInteropSample.csproj deleted file mode 100644 index f9ef4693d5..0000000000 --- a/samples/interop/Direct3DInteropSample/Direct3DInteropSample.csproj +++ /dev/null @@ -1,32 +0,0 @@ - - - Exe - net461 - - - - - - %(Filename) - - - Designer - - - - - - - - PreserveNewest - - - - - - - - - - - diff --git a/samples/interop/Direct3DInteropSample/MainWindow.cs b/samples/interop/Direct3DInteropSample/MainWindow.cs deleted file mode 100644 index 6cc3cb9116..0000000000 --- a/samples/interop/Direct3DInteropSample/MainWindow.cs +++ /dev/null @@ -1,283 +0,0 @@ -using System; - -using Avalonia; -using Avalonia.Controls; -using Avalonia.Direct2D1; -using Avalonia.Direct2D1.Media; -using Avalonia.Markup.Xaml; -using Avalonia.Platform; -using Avalonia.Rendering; - -using SharpDX; -using SharpDX.D3DCompiler; -using SharpDX.Direct2D1; -using SharpDX.Direct3D; -using SharpDX.Direct3D11; -using SharpDX.DXGI; - -using AlphaMode = SharpDX.Direct2D1.AlphaMode; -using Buffer = SharpDX.Direct3D11.Buffer; -using DeviceContext = SharpDX.Direct2D1.DeviceContext; -using Factory2 = SharpDX.DXGI.Factory2; -using InputElement = SharpDX.Direct3D11.InputElement; -using Matrix = SharpDX.Matrix; -using PixelFormat = SharpDX.Direct2D1.PixelFormat; -using Resource = SharpDX.Direct3D11.Resource; - -namespace Direct3DInteropSample -{ - public class MainWindow : Window - { - Texture2D _backBuffer; - RenderTargetView _renderView; - Texture2D _depthBuffer; - DepthStencilView _depthView; - private readonly SwapChain _swapChain; - private SwapChainDescription1 _desc; - private Matrix _proj = Matrix.Identity; - private readonly Matrix _view; - private Buffer _contantBuffer; - private DeviceContext _deviceContext; - private readonly MainWindowViewModel _model; - - public MainWindow() - { - DataContext = _model = new MainWindowViewModel(); - - _desc = new SwapChainDescription1() - { - BufferCount = 1, - Width = (int)ClientSize.Width, - Height = (int)ClientSize.Height, - Format = Format.R8G8B8A8_UNorm, - SampleDescription = new SampleDescription(1, 0), - SwapEffect = SwapEffect.Discard, - Usage = Usage.RenderTargetOutput - }; - - using (var factory = Direct2D1Platform.DxgiDevice.Adapter.GetParent()) - { - _swapChain = new SwapChain1(factory, Direct2D1Platform.DxgiDevice, PlatformImpl?.Handle.Handle ?? IntPtr.Zero, ref _desc); - } - - _deviceContext = new DeviceContext(Direct2D1Platform.Direct2D1Device, DeviceContextOptions.None) - { - DotsPerInch = new Size2F(96, 96) - }; - - CreateMesh(); - - _view = Matrix.LookAtLH(new Vector3(0, 0, -5), new Vector3(0, 0, 0), Vector3.UnitY); - - this.GetObservable(ClientSizeProperty).Subscribe(Resize); - - Resize(ClientSize); - - AvaloniaXamlLoader.Load(this); - - Background = Avalonia.Media.Brushes.Transparent; - } - - - protected override void HandlePaint(Rect rect) - { - var viewProj = Matrix.Multiply(_view, _proj); - var context = Direct2D1Platform.Direct3D11Device.ImmediateContext; - - // Clear views - context.ClearDepthStencilView(_depthView, DepthStencilClearFlags.Depth, 1.0f, 0); - context.ClearRenderTargetView(_renderView, Color.White); - - // Update WorldViewProj Matrix - var worldViewProj = Matrix.RotationX((float)_model.RotationX) * Matrix.RotationY((float)_model.RotationY) - * Matrix.RotationZ((float)_model.RotationZ) - * Matrix.Scaling((float)_model.Zoom) - * viewProj; - worldViewProj.Transpose(); - context.UpdateSubresource(ref worldViewProj, _contantBuffer); - - // Draw the cube - context.Draw(36, 0); - base.HandlePaint(rect); - - // Present! - _swapChain.Present(0, PresentFlags.None); - } - - private void CreateMesh() - { - var device = Direct2D1Platform.Direct3D11Device; - - // Compile Vertex and Pixel shaders - var vertexShaderByteCode = ShaderBytecode.CompileFromFile("MiniCube.fx", "VS", "vs_4_0"); - var vertexShader = new VertexShader(device, vertexShaderByteCode); - - var pixelShaderByteCode = ShaderBytecode.CompileFromFile("MiniCube.fx", "PS", "ps_4_0"); - var pixelShader = new PixelShader(device, pixelShaderByteCode); - - var signature = ShaderSignature.GetInputSignature(vertexShaderByteCode); - - var inputElements = new[] - { - new InputElement("POSITION", 0, Format.R32G32B32A32_Float, 0, 0), - new InputElement("COLOR", 0, Format.R32G32B32A32_Float, 16, 0) - }; - - // Layout from VertexShader input signature - var layout = new InputLayout( - device, - signature, - inputElements); - - // Instantiate Vertex buffer from vertex data - var vertices = Buffer.Create( - device, - BindFlags.VertexBuffer, - new[] - { - new Vector4(-1.0f, -1.0f, -1.0f, 1.0f), new Vector4(1.0f, 0.0f, 0.0f, 1.0f), // Front - new Vector4(-1.0f, 1.0f, -1.0f, 1.0f), new Vector4(1.0f, 0.0f, 0.0f, 1.0f), - new Vector4( 1.0f, 1.0f, -1.0f, 1.0f), new Vector4(1.0f, 0.0f, 0.0f, 1.0f), - new Vector4(-1.0f, -1.0f, -1.0f, 1.0f), new Vector4(1.0f, 0.0f, 0.0f, 1.0f), - new Vector4( 1.0f, 1.0f, -1.0f, 1.0f), new Vector4(1.0f, 0.0f, 0.0f, 1.0f), - new Vector4( 1.0f, -1.0f, -1.0f, 1.0f), new Vector4(1.0f, 0.0f, 0.0f, 1.0f), - - new Vector4(-1.0f, -1.0f, 1.0f, 1.0f), new Vector4(0.0f, 1.0f, 0.0f, 1.0f), // BACK - new Vector4( 1.0f, 1.0f, 1.0f, 1.0f), new Vector4(0.0f, 1.0f, 0.0f, 1.0f), - new Vector4(-1.0f, 1.0f, 1.0f, 1.0f), new Vector4(0.0f, 1.0f, 0.0f, 1.0f), - new Vector4(-1.0f, -1.0f, 1.0f, 1.0f), new Vector4(0.0f, 1.0f, 0.0f, 1.0f), - new Vector4( 1.0f, -1.0f, 1.0f, 1.0f), new Vector4(0.0f, 1.0f, 0.0f, 1.0f), - new Vector4( 1.0f, 1.0f, 1.0f, 1.0f), new Vector4(0.0f, 1.0f, 0.0f, 1.0f), - - new Vector4(-1.0f, 1.0f, -1.0f, 1.0f), new Vector4(0.0f, 0.0f, 1.0f, 1.0f), // Top - new Vector4(-1.0f, 1.0f, 1.0f, 1.0f), new Vector4(0.0f, 0.0f, 1.0f, 1.0f), - new Vector4( 1.0f, 1.0f, 1.0f, 1.0f), new Vector4(0.0f, 0.0f, 1.0f, 1.0f), - new Vector4(-1.0f, 1.0f, -1.0f, 1.0f), new Vector4(0.0f, 0.0f, 1.0f, 1.0f), - new Vector4( 1.0f, 1.0f, 1.0f, 1.0f), new Vector4(0.0f, 0.0f, 1.0f, 1.0f), - new Vector4( 1.0f, 1.0f, -1.0f, 1.0f), new Vector4(0.0f, 0.0f, 1.0f, 1.0f), - - new Vector4(-1.0f, -1.0f, -1.0f, 1.0f), new Vector4(1.0f, 1.0f, 0.0f, 1.0f), // Bottom - new Vector4( 1.0f, -1.0f, 1.0f, 1.0f), new Vector4(1.0f, 1.0f, 0.0f, 1.0f), - new Vector4(-1.0f, -1.0f, 1.0f, 1.0f), new Vector4(1.0f, 1.0f, 0.0f, 1.0f), - new Vector4(-1.0f, -1.0f, -1.0f, 1.0f), new Vector4(1.0f, 1.0f, 0.0f, 1.0f), - new Vector4( 1.0f, -1.0f, -1.0f, 1.0f), new Vector4(1.0f, 1.0f, 0.0f, 1.0f), - new Vector4( 1.0f, -1.0f, 1.0f, 1.0f), new Vector4(1.0f, 1.0f, 0.0f, 1.0f), - - new Vector4(-1.0f, -1.0f, -1.0f, 1.0f), new Vector4(1.0f, 0.0f, 1.0f, 1.0f), // Left - new Vector4(-1.0f, -1.0f, 1.0f, 1.0f), new Vector4(1.0f, 0.0f, 1.0f, 1.0f), - new Vector4(-1.0f, 1.0f, 1.0f, 1.0f), new Vector4(1.0f, 0.0f, 1.0f, 1.0f), - new Vector4(-1.0f, -1.0f, -1.0f, 1.0f), new Vector4(1.0f, 0.0f, 1.0f, 1.0f), - new Vector4(-1.0f, 1.0f, 1.0f, 1.0f), new Vector4(1.0f, 0.0f, 1.0f, 1.0f), - new Vector4(-1.0f, 1.0f, -1.0f, 1.0f), new Vector4(1.0f, 0.0f, 1.0f, 1.0f), - - new Vector4( 1.0f, -1.0f, -1.0f, 1.0f), new Vector4(0.0f, 1.0f, 1.0f, 1.0f), // Right - new Vector4( 1.0f, 1.0f, 1.0f, 1.0f), new Vector4(0.0f, 1.0f, 1.0f, 1.0f), - new Vector4( 1.0f, -1.0f, 1.0f, 1.0f), new Vector4(0.0f, 1.0f, 1.0f, 1.0f), - new Vector4( 1.0f, -1.0f, -1.0f, 1.0f), new Vector4(0.0f, 1.0f, 1.0f, 1.0f), - new Vector4( 1.0f, 1.0f, -1.0f, 1.0f), new Vector4(0.0f, 1.0f, 1.0f, 1.0f), - new Vector4( 1.0f, 1.0f, 1.0f, 1.0f), new Vector4(0.0f, 1.0f, 1.0f, 1.0f), - }); - - // Create Constant Buffer - _contantBuffer = new Buffer(device, Utilities.SizeOf(), ResourceUsage.Default, BindFlags.ConstantBuffer, CpuAccessFlags.None, ResourceOptionFlags.None, 0); - - var context = Direct2D1Platform.Direct3D11Device.ImmediateContext; - - // Prepare All the stages - context.InputAssembler.InputLayout = layout; - context.InputAssembler.PrimitiveTopology = PrimitiveTopology.TriangleList; - context.InputAssembler.SetVertexBuffers(0, new VertexBufferBinding(vertices, Utilities.SizeOf() * 2, 0)); - context.VertexShader.SetConstantBuffer(0, _contantBuffer); - context.VertexShader.Set(vertexShader); - context.PixelShader.Set(pixelShader); - } - - private void Resize(Size size) - { - Utilities.Dispose(ref _deviceContext); - Utilities.Dispose(ref _backBuffer); - Utilities.Dispose(ref _renderView); - Utilities.Dispose(ref _depthBuffer); - Utilities.Dispose(ref _depthView); - var context = Direct2D1Platform.Direct3D11Device.ImmediateContext; - - // Resize the backbuffer - _swapChain.ResizeBuffers(0, 0, 0, Format.Unknown, SwapChainFlags.None); - - // Get the backbuffer from the swapchain - _backBuffer = Resource.FromSwapChain(_swapChain, 0); - - // Renderview on the backbuffer - _renderView = new RenderTargetView(Direct2D1Platform.Direct3D11Device, _backBuffer); - - // Create the depth buffer - _depthBuffer = new Texture2D( - Direct2D1Platform.Direct3D11Device, - new Texture2DDescription() - { - Format = Format.D32_Float_S8X24_UInt, - ArraySize = 1, - MipLevels = 1, - Width = (int)size.Width, - Height = (int)size.Height, - SampleDescription = new SampleDescription(1, 0), - Usage = ResourceUsage.Default, - BindFlags = BindFlags.DepthStencil, - CpuAccessFlags = CpuAccessFlags.None, - OptionFlags = ResourceOptionFlags.None - }); - - // Create the depth buffer view - _depthView = new DepthStencilView(Direct2D1Platform.Direct3D11Device, _depthBuffer); - - // Setup targets and viewport for rendering - context.Rasterizer.SetViewport(new Viewport(0, 0, (int)size.Width, (int)size.Height, 0.0f, 1.0f)); - context.OutputMerger.SetTargets(_depthView, _renderView); - - // Setup new projection matrix with correct aspect ratio - _proj = Matrix.PerspectiveFovLH((float)Math.PI / 4.0f, (float)(size.Width / size.Height), 0.1f, 100.0f); - - using (var dxgiBackBuffer = _swapChain.GetBackBuffer(0)) - { - var renderTarget = new SharpDX.Direct2D1.RenderTarget( - Direct2D1Platform.Direct2D1Factory, - dxgiBackBuffer, - new RenderTargetProperties - { - DpiX = 96, - DpiY = 96, - Type = RenderTargetType.Default, - PixelFormat = new PixelFormat( - Format.Unknown, - AlphaMode.Premultiplied) - }); - - _deviceContext = renderTarget.QueryInterface(); - - renderTarget.Dispose(); - } - } - - private class D3DRenderTarget : IRenderTarget - { - private readonly MainWindow _window; - - public D3DRenderTarget(MainWindow window) - { - _window = window; - } - - public void Dispose() - { - } - - public IDrawingContextImpl CreateDrawingContext(IVisualBrushRenderer visualBrushRenderer) - { - return new DrawingContextImpl(visualBrushRenderer, null, _window._deviceContext); - } - } - - - protected override IRenderTarget CreateRenderTarget() => new D3DRenderTarget(this); - } -} diff --git a/samples/interop/Direct3DInteropSample/MainWindow.paml b/samples/interop/Direct3DInteropSample/MainWindow.paml deleted file mode 100644 index 37c6265836..0000000000 --- a/samples/interop/Direct3DInteropSample/MainWindow.paml +++ /dev/null @@ -1,14 +0,0 @@ - - - - Rotation X - - Rotation Y - - Rotation Z - - Zoom - - - - \ No newline at end of file diff --git a/samples/interop/Direct3DInteropSample/MainWindowViewModel.cs b/samples/interop/Direct3DInteropSample/MainWindowViewModel.cs deleted file mode 100644 index 21679a99c5..0000000000 --- a/samples/interop/Direct3DInteropSample/MainWindowViewModel.cs +++ /dev/null @@ -1,45 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using MiniMvvm; - -namespace Direct3DInteropSample -{ - public class MainWindowViewModel : ViewModelBase - { - private double _rotationX; - - public double RotationX - { - get { return _rotationX; } - set { this.RaiseAndSetIfChanged(ref _rotationX, value); } - } - - private double _rotationY = 1; - - public double RotationY - { - get { return _rotationY; } - set { this.RaiseAndSetIfChanged(ref _rotationY, value); } - } - - private double _rotationZ = 2; - - public double RotationZ - { - get { return _rotationZ; } - set { this.RaiseAndSetIfChanged(ref _rotationZ, value); } - } - - - private double _zoom = 1; - - public double Zoom - { - get { return _zoom; } - set { this.RaiseAndSetIfChanged(ref _zoom, value); } - } - } -} diff --git a/samples/interop/Direct3DInteropSample/MiniCube.fx b/samples/interop/Direct3DInteropSample/MiniCube.fx deleted file mode 100644 index f246421f2d..0000000000 --- a/samples/interop/Direct3DInteropSample/MiniCube.fx +++ /dev/null @@ -1,47 +0,0 @@ -// Copyright (c) 2010-2013 SharpDX - Alexandre Mutel -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -// THE SOFTWARE. -struct VS_IN -{ - float4 pos : POSITION; - float4 col : COLOR; -}; - -struct PS_IN -{ - float4 pos : SV_POSITION; - float4 col : COLOR; -}; - -float4x4 worldViewProj; - -PS_IN VS( VS_IN input ) -{ - PS_IN output = (PS_IN)0; - - output.pos = mul(input.pos, worldViewProj); - output.col = input.col; - - return output; -} - -float4 PS( PS_IN input ) : SV_Target -{ - return input.col; -} \ No newline at end of file diff --git a/samples/interop/Direct3DInteropSample/Program.cs b/samples/interop/Direct3DInteropSample/Program.cs deleted file mode 100644 index bf8e76d7e4..0000000000 --- a/samples/interop/Direct3DInteropSample/Program.cs +++ /dev/null @@ -1,16 +0,0 @@ -using Avalonia; - -namespace Direct3DInteropSample -{ - class Program - { - public static AppBuilder BuildAvaloniaApp() - => AppBuilder.Configure() - .With(new Win32PlatformOptions { UseDeferredRendering = false }) - .UseWin32() - .UseDirect2D1(); - - public static int Main(string[] args) - => BuildAvaloniaApp().StartWithClassicDesktopLifetime(args); - } -} diff --git a/src/Android/Avalonia.Android/AndroidInputMethod.cs b/src/Android/Avalonia.Android/AndroidInputMethod.cs index 8d56086470..c885a7768c 100644 --- a/src/Android/Avalonia.Android/AndroidInputMethod.cs +++ b/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; diff --git a/src/Android/Avalonia.Android/AndroidPlatform.cs b/src/Android/Avalonia.Android/AndroidPlatform.cs index 2b6d29e7c5..75856e4b52 100644 --- a/src/Android/Avalonia.Android/AndroidPlatform.cs +++ b/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(), - AvaloniaLocator.Current.GetService()); + AvaloniaLocator.Current.GetService()); } + else + RenderInterface = + new PlatformRenderInterfaceContextManager(AvaloniaLocator.Current + .GetService()); } } diff --git a/src/Android/Avalonia.Android/OpenGL/GlPlatformSurface.cs b/src/Android/Avalonia.Android/OpenGL/GlPlatformSurface.cs deleted file mode 100644 index e85ed11028..0000000000 --- a/src/Android/Avalonia.Android/OpenGL/GlPlatformSurface.cs +++ /dev/null @@ -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(); - if (feature is EglPlatformOpenGlInterface egl) - { - return new GlPlatformSurface(egl, info); - } - - return null; - } - } -} diff --git a/src/Android/Avalonia.Android/OpenGL/GlRenderTarget.cs b/src/Android/Avalonia.Android/OpenGL/GlRenderTarget.cs deleted file mode 100644 index f9071d9b27..0000000000 --- a/src/Android/Avalonia.Android/OpenGL/GlRenderTarget.cs +++ /dev/null @@ -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); - } -} diff --git a/src/Android/Avalonia.Android/Platform/SkiaPlatform/AndroidFramebuffer.cs b/src/Android/Avalonia.Android/Platform/SkiaPlatform/AndroidFramebuffer.cs index aabf8160f8..94e5f4bd01 100644 --- a/src/Android/Avalonia.Android/Platform/SkiaPlatform/AndroidFramebuffer.cs +++ b/src/Android/Avalonia.Android/Platform/SkiaPlatform/AndroidFramebuffer.cs @@ -96,12 +96,14 @@ namespace Avalonia.Android.Platform.SkiaPlatform public IntPtr bits; // Do not touch. +#pragma warning disable CA1823 // Avoid unused private fields uint reserved1; uint reserved2; uint reserved3; uint reserved4; uint reserved5; uint reserved6; +#pragma warning restore CA1823 // Avoid unused private fields } } } diff --git a/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs b/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs index 4150b52946..56dbadca03 100644 --- a/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs +++ b/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(_view); _keyboardHelper = new AndroidKeyboardEventsHelper(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()) { RenderOnlyOnRenderThread = true } - : new ImmediateRenderer((Visual)root); + ? new DeferredRenderer(root, AvaloniaLocator.Current.GetRequiredService(), + () => 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; diff --git a/src/Avalonia.Base/Animation/Animators/Animator`1.cs b/src/Avalonia.Base/Animation/Animators/Animator`1.cs index 8765cfb4c9..b5d1feb4a7 100644 --- a/src/Avalonia.Base/Animation/Animators/Animator`1.cs +++ b/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(this, animation, control, clock, onComplete); - return match.Subscribe(subject); + return new CompositeDisposable(match.Subscribe(subject), subject); } protected T InterpolationHandler(double animationTime, T neutralValue) diff --git a/src/Avalonia.Base/Animation/Cue.cs b/src/Avalonia.Base/Animation/Cue.cs index 6578148b07..c48f2ab6b0 100644 --- a/src/Avalonia.Base/Animation/Cue.cs +++ b/src/Avalonia.Base/Animation/Cue.cs @@ -8,7 +8,7 @@ namespace Avalonia.Animation /// Determines the time index for a . /// [TypeConverter(typeof(CueTypeConverter))] - public readonly struct Cue : IEquatable, IEquatable + public readonly record struct Cue : IEquatable, IEquatable { /// /// The normalized percent value, ranging from 0.0 to 1.0 @@ -49,15 +49,6 @@ namespace Avalonia.Animation } } - /// - /// Checks for equality between two s. - /// - /// The second cue. - public bool Equals(Cue other) - { - return CueValue == other.CueValue; - } - /// /// Checks for equality between a /// and a value. diff --git a/src/Avalonia.Base/CornerRadius.cs b/src/Avalonia.Base/CornerRadius.cs index d56a0ef19d..1666fac2e1 100644 --- a/src/Avalonia.Base/CornerRadius.cs +++ b/src/Avalonia.Base/CornerRadius.cs @@ -61,9 +61,13 @@ namespace Avalonia public double BottomLeft { get; } /// - /// Gets a value indicating whether all corner radii are set to 0. + /// Gets a value indicating whether the instance has default values (all corner radii are set to 0). /// - public bool IsEmpty => TopLeft.Equals(0) && IsUniform; + public bool IsDefault => TopLeft == 0 && TopRight == 0 && BottomLeft == 0 && BottomRight == 0; + + /// + [Obsolete("Use IsDefault instead.")] + public bool IsEmpty => IsDefault; /// /// Gets a value indicating whether all corner radii are equal. @@ -79,7 +83,6 @@ namespace Avalonia { // ReSharper disable CompareOfFloatsByEqualityOperator return TopLeft == other.TopLeft && - TopRight == other.TopRight && BottomRight == other.BottomRight && BottomLeft == other.BottomLeft; diff --git a/src/Avalonia.Base/Data/BindingValue.cs b/src/Avalonia.Base/Data/BindingValue.cs index 4e07ebf445..3bc172f596 100644 --- a/src/Avalonia.Base/Data/BindingValue.cs +++ b/src/Avalonia.Base/Data/BindingValue.cs @@ -80,7 +80,7 @@ namespace Avalonia.Data /// - For an unset value, use or simply `default` /// - For other types, call one of the static factory methods /// - public readonly struct BindingValue + public readonly record struct BindingValue { private readonly T _value; diff --git a/src/Avalonia.Base/Data/Core/ExpressionNode.cs b/src/Avalonia.Base/Data/Core/ExpressionNode.cs index e4b833176c..5fb2bb5c13 100644 --- a/src/Avalonia.Base/Data/Core/ExpressionNode.cs +++ b/src/Avalonia.Base/Data/Core/ExpressionNode.cs @@ -4,8 +4,6 @@ namespace Avalonia.Data.Core { public abstract class ExpressionNode { - private static readonly object CacheInvalid = new object(); - protected static readonly WeakReference UnsetReference = new WeakReference(AvaloniaProperty.UnsetValue); diff --git a/src/Avalonia.Base/Data/Core/ExpressionObserver.cs b/src/Avalonia.Base/Data/Core/ExpressionObserver.cs index 0a9f834aeb..2151c100e5 100644 --- a/src/Avalonia.Base/Data/Core/ExpressionObserver.cs +++ b/src/Avalonia.Base/Data/Core/ExpressionObserver.cs @@ -49,8 +49,6 @@ namespace Avalonia.Data.Core new TaskStreamPlugin(), new ObservableStreamPlugin(), }; - - private static readonly object UninitializedValue = new object(); private readonly ExpressionNode _node; private object? _root; private Func? _rootGetter; diff --git a/src/Avalonia.Base/Input/GestureRecognizers/PullGestureRecognizer.cs b/src/Avalonia.Base/Input/GestureRecognizers/PullGestureRecognizer.cs new file mode 100644 index 0000000000..fedd07ec32 --- /dev/null +++ b/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; + + /// + /// Defines the property. + /// + public static readonly DirectProperty PullDirectionProperty = + AvaloniaProperty.RegisterDirect( + 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)); + } + } +} diff --git a/src/Avalonia.Base/Input/Gestures.cs b/src/Avalonia.Base/Input/Gestures.cs index 496052ad35..1ea88fe824 100644 --- a/src/Avalonia.Base/Input/Gestures.cs +++ b/src/Avalonia.Base/Input/Gestures.cs @@ -46,6 +46,14 @@ namespace Avalonia.Input private static readonly WeakReference s_lastPress = new WeakReference(null); private static Point s_lastPressPoint; + public static readonly RoutedEvent PullGestureEvent = + RoutedEvent.Register( + "PullGesture", RoutingStrategies.Bubble, typeof(Gestures)); + + public static readonly RoutedEvent PullGestureEndedEvent = + RoutedEvent.Register( + "PullGestureEnded", RoutingStrategies.Bubble, typeof(Gestures)); + static Gestures() { InputElement.PointerPressedEvent.RouteFinished.Subscribe(PointerPressed); diff --git a/src/Avalonia.Base/Input/InputElement.cs b/src/Avalonia.Base/Input/InputElement.cs index d0130258c3..fa755277cc 100644 --- a/src/Avalonia.Base/Input/InputElement.cs +++ b/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); + } } } diff --git a/src/Avalonia.Base/Input/PointerPoint.cs b/src/Avalonia.Base/Input/PointerPoint.cs index c51f286053..bd36b73b26 100644 --- a/src/Avalonia.Base/Input/PointerPoint.cs +++ b/src/Avalonia.Base/Input/PointerPoint.cs @@ -5,7 +5,7 @@ namespace Avalonia.Input /// /// Provides basic properties for the input pointer associated with a single mouse, pen/stylus, or touch contact. /// - public struct PointerPoint + public record struct PointerPoint { public PointerPoint(IPointer pointer, Point position, PointerPointProperties properties) { @@ -33,7 +33,7 @@ namespace Avalonia.Input /// /// Provides extended properties for a PointerPoint object. /// - public struct PointerPointProperties + public record struct PointerPointProperties { /// /// Gets a value that indicates whether the pointer input was triggered by the primary action mode of an input device. diff --git a/src/Avalonia.Base/Input/PullGestureEventArgs.cs b/src/Avalonia.Base/Input/PullGestureEventArgs.cs new file mode 100644 index 0000000000..34d95c87f4 --- /dev/null +++ b/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 + } +} diff --git a/src/Avalonia.Base/Input/Raw/RawPointerEventArgs.cs b/src/Avalonia.Base/Input/Raw/RawPointerEventArgs.cs index b8f6f99ae8..854dd4b83b 100644 --- a/src/Avalonia.Base/Input/Raw/RawPointerEventArgs.cs +++ b/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 { /// /// Pointer position, in client DIPs. diff --git a/src/Avalonia.Base/Input/TextInput/ITextInputMethodClient.cs b/src/Avalonia.Base/Input/TextInput/ITextInputMethodClient.cs index 4a60f8a046..531cf3c704 100644 --- a/src/Avalonia.Base/Input/TextInput/ITextInputMethodClient.cs +++ b/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; } diff --git a/src/Avalonia.Base/Logging/ParametrizedLogger.cs b/src/Avalonia.Base/Logging/ParametrizedLogger.cs index 6b7331b504..a82af41a75 100644 --- a/src/Avalonia.Base/Logging/ParametrizedLogger.cs +++ b/src/Avalonia.Base/Logging/ParametrizedLogger.cs @@ -5,7 +5,7 @@ namespace Avalonia.Logging /// /// Logger sink parametrized for given logging level. /// - public readonly struct ParametrizedLogger + public readonly record struct ParametrizedLogger { private readonly ILogSink _sink; private readonly LogEventLevel _level; diff --git a/src/Avalonia.Base/Matrix.cs b/src/Avalonia.Base/Matrix.cs index df6439b1b3..a72710ffd4 100644 --- a/src/Avalonia.Base/Matrix.cs +++ b/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; diff --git a/src/Avalonia.Base/Media/BoxShadow.cs b/src/Avalonia.Base/Media/BoxShadow.cs index 2139516be6..dd2c23f4ae 100644 --- a/src/Avalonia.Base/Media/BoxShadow.cs +++ b/src/Avalonia.Base/Media/BoxShadow.cs @@ -45,7 +45,14 @@ namespace Avalonia.Media } } - public bool IsEmpty => OffsetX == 0 && OffsetY == 0 && Blur == 0 && Spread == 0; + /// + /// Gets a value indicating whether the instance has default values. + /// + public bool IsDefault => OffsetX == 0 && OffsetY == 0 && Blur == 0 && Spread == 0; + + /// + [Obsolete("Use IsDefault instead.")] + public bool IsEmpty => IsDefault; private readonly static char[] s_Separator = new char[] { ' ', '\t' }; @@ -82,7 +89,7 @@ namespace Avalonia.Media { var sb = StringBuilderCache.Acquire(); - if (IsEmpty) + if (IsDefault) { return "none"; } @@ -171,5 +178,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); } } diff --git a/src/Avalonia.Base/Media/BoxShadows.cs b/src/Avalonia.Base/Media/BoxShadows.cs index ab2694389f..50ae7699dd 100644 --- a/src/Avalonia.Base/Media/BoxShadows.cs +++ b/src/Avalonia.Base/Media/BoxShadows.cs @@ -21,7 +21,7 @@ namespace Avalonia.Media { _first = shadow; _list = null; - Count = _first.IsEmpty ? 0 : 1; + Count = _first.IsDefault ? 0 : 1; } public BoxShadows(BoxShadow first, BoxShadow[] rest) @@ -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; @@ -118,7 +120,7 @@ namespace Avalonia.Media get { foreach(var boxShadow in this) - if (!boxShadow.IsEmpty && boxShadow.IsInset) + if (!boxShadow.IsDefault && boxShadow.IsInset) return true; return false; } @@ -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); } } diff --git a/src/Avalonia.Base/Media/DashStyle.cs b/src/Avalonia.Base/Media/DashStyle.cs index 3a30b2d32f..9c30b6f872 100644 --- a/src/Avalonia.Base/Media/DashStyle.cs +++ b/src/Avalonia.Base/Media/DashStyle.cs @@ -17,8 +17,8 @@ namespace Avalonia.Media /// /// Defines the property. /// - public static readonly StyledProperty> DashesProperty = - AvaloniaProperty.Register>(nameof(Dashes)); + public static readonly StyledProperty?> DashesProperty = + AvaloniaProperty.Register?>(nameof(Dashes)); /// /// Defines the property. @@ -83,7 +83,7 @@ namespace Avalonia.Media /// /// Gets or sets the length of alternating dashes and gaps. /// - public AvaloniaList Dashes + public AvaloniaList? Dashes { get => GetValue(DashesProperty); set => SetValue(DashesProperty, value); @@ -98,7 +98,7 @@ namespace Avalonia.Media set => SetValue(OffsetProperty, value); } - IReadOnlyList IDashStyle.Dashes => Dashes; + IReadOnlyList? IDashStyle.Dashes => Dashes; /// /// Raised when the dash style changes. diff --git a/src/Avalonia.Base/Media/DrawingContext.cs b/src/Avalonia.Base/Media/DrawingContext.cs index d0f3b9e21a..eabd7c8274 100644 --- a/src/Avalonia.Base/Media/DrawingContext.cs +++ b/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; diff --git a/src/Avalonia.Base/Media/FontMetrics.cs b/src/Avalonia.Base/Media/FontMetrics.cs index 1cd01675db..6d952a6b93 100644 --- a/src/Avalonia.Base/Media/FontMetrics.cs +++ b/src/Avalonia.Base/Media/FontMetrics.cs @@ -3,7 +3,7 @@ /// /// The font metrics is holding information about a font's ascent, descent, etc. in design em units. /// - public readonly struct FontMetrics + public readonly record struct FontMetrics { /// /// Gets the font design units per em. diff --git a/src/Avalonia.Base/Media/FormattedText.cs b/src/Avalonia.Base/Media/FormattedText.cs index 90b9755493..774580415a 100644 --- a/src/Avalonia.Base/Media/FormattedText.cs +++ b/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 _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(textToFormat.AsMemory()) : - throw new ArgumentNullException(nameof(textToFormat)); + _text = textToFormat; var runProps = new GenericTextRunProperties( typeface, @@ -1383,7 +1379,7 @@ namespace Avalonia.Media } } - if (accumulatedBounds?.PlatformImpl == null || accumulatedBounds.PlatformImpl.Bounds.IsEmpty) + if (accumulatedBounds?.PlatformImpl == null || accumulatedBounds.PlatformImpl.Bounds.IsDefault) { return null; } diff --git a/src/Avalonia.Base/Media/Geometry.cs b/src/Avalonia.Base/Media/Geometry.cs index c31a6699c2..2019f54c70 100644 --- a/src/Avalonia.Base/Media/Geometry.cs +++ b/src/Avalonia.Base/Media/Geometry.cs @@ -30,7 +30,7 @@ namespace Avalonia.Media /// /// Gets the geometry's bounding rectangle. /// - public Rect Bounds => PlatformImpl?.Bounds ?? Rect.Empty; + public Rect Bounds => PlatformImpl?.Bounds ?? default; /// /// Gets the platform-specific implementation of the geometry. @@ -84,7 +84,7 @@ namespace Avalonia.Media /// /// The stroke thickness. /// The bounding rectangle. - public Rect GetRenderBounds(IPen pen) => PlatformImpl?.GetRenderBounds(pen) ?? Rect.Empty; + public Rect GetRenderBounds(IPen pen) => PlatformImpl?.GetRenderBounds(pen) ?? default; /// /// Indicates whether the geometry's fill contains the specified point. diff --git a/src/Avalonia.Base/Media/GeometryDrawing.cs b/src/Avalonia.Base/Media/GeometryDrawing.cs index 7df7d25954..26cc2c3cab 100644 --- a/src/Avalonia.Base/Media/GeometryDrawing.cs +++ b/src/Avalonia.Base/Media/GeometryDrawing.cs @@ -69,7 +69,7 @@ namespace Avalonia.Media public override Rect GetBounds() { IPen pen = Pen ?? s_boundsPen; - return Geometry?.GetRenderBounds(pen) ?? Rect.Empty; + return Geometry?.GetRenderBounds(pen) ?? default; } } } diff --git a/src/Avalonia.Base/Media/GlyphMetrics.cs b/src/Avalonia.Base/Media/GlyphMetrics.cs index 2ee1f87d38..e9b5a112ac 100644 --- a/src/Avalonia.Base/Media/GlyphMetrics.cs +++ b/src/Avalonia.Base/Media/GlyphMetrics.cs @@ -1,6 +1,6 @@ namespace Avalonia.Media; -public readonly struct GlyphMetrics +public readonly record struct GlyphMetrics { /// /// Distance from the x-origin to the left extremum of the glyph. diff --git a/src/Avalonia.Base/Media/GlyphRun.cs b/src/Avalonia.Base/Media/GlyphRun.cs index d93a68e78b..af9e458a28 100644 --- a/src/Avalonia.Base/Media/GlyphRun.cs +++ b/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 _characters; - + private IReadOnlyList _characters; private IReadOnlyList _glyphIndices; private IReadOnlyList? _glyphAdvances; private IReadOnlyList? _glyphOffsets; private IReadOnlyList? _glyphClusters; - private int _offsetToFirstCharacter; - /// /// Initializes a new instance of the class by specifying properties of the class. /// @@ -45,7 +41,7 @@ namespace Avalonia.Media public GlyphRun( IGlyphTypeface glyphTypeface, double fontRenderingEmSize, - ReadOnlySlice characters, + IReadOnlyList characters, IReadOnlyList glyphIndices, IReadOnlyList? glyphAdvances = null, IReadOnlyList? 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; } /// @@ -145,7 +141,7 @@ namespace Avalonia.Media /// /// Gets or sets the list of UTF16 code points that represent the Unicode content of the . /// - public ReadOnlySlice Characters + public IReadOnlyList Characters { get => _characters; set => Set(ref _characters, value); @@ -219,7 +215,7 @@ namespace Avalonia.Media /// 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 /// 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 _); } /// @@ -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); } /// @@ -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(); - return platformRenderInterface.CreateGlyphRun(GlyphTypeface, FontRenderingEmSize, GlyphIndices, GlyphAdvances, GlyphOffsets); + _glyphRunImpl = platformRenderInterface.CreateGlyphRun(GlyphTypeface, FontRenderingEmSize, GlyphIndices, GlyphAdvances, GlyphOffsets); } void IDisposable.Dispose() diff --git a/src/Avalonia.Base/Media/GlyphRunDrawing.cs b/src/Avalonia.Base/Media/GlyphRunDrawing.cs index 7e0d5c3c81..242b9913fa 100644 --- a/src/Avalonia.Base/Media/GlyphRunDrawing.cs +++ b/src/Avalonia.Base/Media/GlyphRunDrawing.cs @@ -32,7 +32,7 @@ public override Rect GetBounds() { - return GlyphRun != null ? new Rect(GlyphRun.Size) : Rect.Empty; + return GlyphRun != null ? new Rect(GlyphRun.Size) : default; } } } diff --git a/src/Avalonia.Base/Media/GlyphRunMetrics.cs b/src/Avalonia.Base/Media/GlyphRunMetrics.cs index a8698a7d82..492b5214cd 100644 --- a/src/Avalonia.Base/Media/GlyphRunMetrics.cs +++ b/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; } } } diff --git a/src/Avalonia.Base/Media/IDashStyle.cs b/src/Avalonia.Base/Media/IDashStyle.cs index 7208216603..b988ad210a 100644 --- a/src/Avalonia.Base/Media/IDashStyle.cs +++ b/src/Avalonia.Base/Media/IDashStyle.cs @@ -12,7 +12,7 @@ namespace Avalonia.Media /// /// Gets or sets the length of alternating dashes and gaps. /// - IReadOnlyList Dashes { get; } + IReadOnlyList? Dashes { get; } /// /// Gets or sets how far in the dash sequence the stroke will start. diff --git a/src/Avalonia.Base/Media/ImageDrawing.cs b/src/Avalonia.Base/Media/ImageDrawing.cs index 82f97b52b4..d3e5c4841b 100644 --- a/src/Avalonia.Base/Media/ImageDrawing.cs +++ b/src/Avalonia.Base/Media/ImageDrawing.cs @@ -42,7 +42,7 @@ namespace Avalonia.Media var imageSource = ImageSource; var rect = Rect; - if (imageSource is object && !rect.IsEmpty) + if (imageSource is object && !rect.IsDefault) { context.DrawImage(imageSource, rect); } diff --git a/src/Avalonia.Base/Media/Imaging/CroppedBitmap.cs b/src/Avalonia.Base/Media/Imaging/CroppedBitmap.cs index 70f9fbf567..525a543b70 100644 --- a/src/Avalonia.Base/Media/Imaging/CroppedBitmap.cs +++ b/src/Avalonia.Base/Media/Imaging/CroppedBitmap.cs @@ -78,8 +78,8 @@ namespace Avalonia.Media.Imaging get { if (Source is not IBitmap bmp) - return Size.Empty; - if (SourceRect.IsEmpty) + return default; + if (SourceRect.IsDefault) return Source.Size; return SourceRect.Size.ToSizeWithDpi(bmp.Dpi); } diff --git a/src/Avalonia.Base/Media/Imaging/RenderTargetBitmap.cs b/src/Avalonia.Base/Media/Imaging/RenderTargetBitmap.cs index d56711ad68..88e5e627ee 100644 --- a/src/Avalonia.Base/Media/Imaging/RenderTargetBitmap.cs +++ b/src/Avalonia.Base/Media/Imaging/RenderTargetBitmap.cs @@ -60,5 +60,7 @@ namespace Avalonia.Media.Imaging /// public IDrawingContextImpl CreateDrawingContext(IVisualBrushRenderer? vbr) => PlatformImpl.Item.CreateDrawingContext(vbr); + + bool IRenderTarget.IsCorrupted => false; } } diff --git a/src/Avalonia.Base/Media/Immutable/ImmutableDashStyle.cs b/src/Avalonia.Base/Media/Immutable/ImmutableDashStyle.cs index 82485c13b0..1f53f06955 100644 --- a/src/Avalonia.Base/Media/Immutable/ImmutableDashStyle.cs +++ b/src/Avalonia.Base/Media/Immutable/ImmutableDashStyle.cs @@ -17,7 +17,7 @@ namespace Avalonia.Media.Immutable /// /// The dashes collection. /// The dash sequence offset. - public ImmutableDashStyle(IEnumerable dashes, double offset) + public ImmutableDashStyle(IEnumerable? dashes, double offset) { _dashes = dashes?.ToArray() ?? Array.Empty(); Offset = offset; @@ -69,7 +69,7 @@ namespace Avalonia.Media.Immutable return hashCode; } - private static bool SequenceEqual(IReadOnlyList left, IReadOnlyList right) + private static bool SequenceEqual(IReadOnlyList left, IReadOnlyList? right) { if (ReferenceEquals(left, right)) { diff --git a/src/Avalonia.Base/Media/TextCollapsingCreateInfo.cs b/src/Avalonia.Base/Media/TextCollapsingCreateInfo.cs index 78f15b724a..40ba613717 100644 --- a/src/Avalonia.Base/Media/TextCollapsingCreateInfo.cs +++ b/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; diff --git a/src/Avalonia.Base/Media/TextFormatting/CharacterBufferRange.cs b/src/Avalonia.Base/Media/TextFormatting/CharacterBufferRange.cs new file mode 100644 index 0000000000..d76f212f26 --- /dev/null +++ b/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 + { + /// + /// Getting an empty character string + /// + public static CharacterBufferRange Empty => new CharacterBufferRange(); + + /// + /// Construct from character array + /// + /// character array + /// character buffer offset to the first character + /// character length + public CharacterBufferRange( + char[] characterArray, + int offsetToFirstChar, + int characterLength + ) + : this( + new CharacterBufferReference(characterArray, offsetToFirstChar), + characterLength + ) + { } + + /// + /// Construct from string + /// + /// character string + /// character buffer offset to the first character + /// character length + public CharacterBufferRange( + string characterString, + int offsetToFirstChar, + int characterLength + ) + : this( + new CharacterBufferReference(characterString, offsetToFirstChar), + characterLength + ) + { } + + /// + /// Construct a from + /// + /// character buffer reference + /// number of characters + 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; + } + + /// + /// Construct a from part of another + /// + internal CharacterBufferRange( + CharacterBufferRange characterBufferRange, + int offsetToFirstChar, + int characterLength + ) : + this( + characterBufferRange.CharacterBuffer, + characterBufferRange.OffsetToFirstChar + offsetToFirstChar, + characterLength + ) + { } + + + /// + /// Construct a from string + /// + internal CharacterBufferRange( + string charString + ) : + this( + charString, + 0, + charString.Length + ) + { } + + + /// + /// Construct from memory buffer + /// + internal CharacterBufferRange( + ReadOnlyMemory charBuffer, + int offsetToFirstChar, + int characterLength + ) : + this( + new CharacterBufferReference(charBuffer, offsetToFirstChar), + characterLength + ) + { } + + + /// + /// Construct a by extracting text info from a text run + /// + 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]; + } + } + + /// + /// Gets a reference to the character buffer + /// + public CharacterBufferReference CharacterBufferReference { get; } + + /// + /// Gets the number of characters in text source character store + /// + public int Length { get; } + + /// + /// Gets a span from the character buffer range + /// + public ReadOnlySpan Span => + CharacterBufferReference.CharacterBuffer.Span.Slice(CharacterBufferReference.OffsetToFirstChar, Length); + + /// + /// Gets the character memory buffer + /// + internal ReadOnlyMemory CharacterBuffer + { + get { return CharacterBufferReference.CharacterBuffer; } + } + + /// + /// Gets the character offset relative to the beginning of buffer to + /// the first character of the run + /// + internal int OffsetToFirstChar + { + get { return CharacterBufferReference.OffsetToFirstChar; } + } + + /// + /// Indicate whether the character buffer range is empty + /// + 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); + } + + /// + /// Compute hash code + /// + public override int GetHashCode() + { + return CharacterBufferReference.GetHashCode() ^ Length; + } + + /// + /// Test equality with the input object + /// + /// The object to test + public override bool Equals(object? obj) + { + if (obj is CharacterBufferRange range) + { + return Equals(range); + } + + return false; + } + + /// + /// Test equality with the input CharacterBufferRange + /// + /// The CharacterBufferRange value to test + public bool Equals(CharacterBufferRange value) + { + return CharacterBufferReference.Equals(value.CharacterBufferReference) + && Length == value.Length; + } + + /// + /// Compare two CharacterBufferRange for equality + /// + /// left operand + /// right operand + /// whether or not two operands are equal + public static bool operator ==(CharacterBufferRange left, CharacterBufferRange right) + { + return left.Equals(right); + } + + /// + /// Compare two CharacterBufferRange for inequality + /// + /// left operand + /// right operand + /// whether or not two operands are equal + public static bool operator !=(CharacterBufferRange left, CharacterBufferRange right) + { + return !(left == right); + } + + int IReadOnlyCollection.Count => Length; + + public IEnumerator GetEnumerator() + { + return new ImmutableReadOnlyListStructEnumerator(this); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + } +} diff --git a/src/Avalonia.Base/Media/TextFormatting/CharacterBufferReference.cs b/src/Avalonia.Base/Media/TextFormatting/CharacterBufferReference.cs new file mode 100644 index 0000000000..672fcf3377 --- /dev/null +++ b/src/Avalonia.Base/Media/TextFormatting/CharacterBufferReference.cs @@ -0,0 +1,115 @@ +using System; + +namespace Avalonia.Media.TextFormatting +{ + /// + /// Text character buffer reference + /// + public readonly struct CharacterBufferReference : IEquatable + { + /// + /// Construct character buffer reference from character array + /// + /// character array + /// character buffer offset to the first character + public CharacterBufferReference(char[] characterArray, int offsetToFirstChar = 0) + : this(characterArray.AsMemory(), offsetToFirstChar) + { } + + /// + /// Construct character buffer reference from string + /// + /// character string + /// character buffer offset to the first character + public CharacterBufferReference(string characterString, int offsetToFirstChar = 0) + : this(characterString.AsMemory(), offsetToFirstChar) + { } + + /// + /// Construct character buffer reference from memory buffer + /// + internal CharacterBufferReference(ReadOnlyMemory 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; + } + + /// + /// Gets the character memory buffer + /// + public ReadOnlyMemory CharacterBuffer { get; } + + /// + /// Gets the character offset relative to the beginning of buffer to + /// the first character of the run + /// + public int OffsetToFirstChar { get; } + + /// + /// Compute hash code + /// + public override int GetHashCode() + { + return CharacterBuffer.IsEmpty ? 0 : CharacterBuffer.GetHashCode(); + } + + /// + /// Test equality with the input object + /// + /// The object to test. + public override bool Equals(object? obj) + { + if (obj is CharacterBufferReference reference) + { + return Equals(reference); + } + + return false; + } + + /// + /// Test equality with the input CharacterBufferReference + /// + /// The characterBufferReference value to test + public bool Equals(CharacterBufferReference value) + { + return CharacterBuffer.Equals(value.CharacterBuffer); + } + + /// + /// Compare two CharacterBufferReference for equality + /// + /// left operand + /// right operand + /// whether or not two operands are equal + public static bool operator ==(CharacterBufferReference left, CharacterBufferReference right) + { + return left.Equals(right); + } + + /// + /// Compare two CharacterBufferReference for inequality + /// + /// left operand + /// right operand + /// whether or not two operands are equal + public static bool operator !=(CharacterBufferReference left, CharacterBufferReference right) + { + return !(left == right); + } + } +} + diff --git a/src/Avalonia.Base/Media/TextFormatting/FormattedTextSource.cs b/src/Avalonia.Base/Media/TextFormatting/FormattedTextSource.cs index fb8e699d8e..4472ba87eb 100644 --- a/src/Avalonia.Base/Media/TextFormatting/FormattedTextSource.cs +++ b/src/Avalonia.Base/Media/TextFormatting/FormattedTextSource.cs @@ -7,14 +7,14 @@ namespace Avalonia.Media.TextFormatting { internal readonly struct FormattedTextSource : ITextSource { - private readonly ReadOnlySlice _text; + private readonly CharacterBufferRange _text; private readonly TextRunProperties _defaultProperties; private readonly IReadOnlyList>? _textModifier; - public FormattedTextSource(ReadOnlySlice text, TextRunProperties defaultProperties, + public FormattedTextSource(string text, TextRunProperties defaultProperties, IReadOnlyList>? textModifier) { - _text = text; + _text = new CharacterBufferRange(text); _defaultProperties = defaultProperties; _textModifier = textModifier; } @@ -35,7 +35,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); } /// @@ -48,7 +48,7 @@ namespace Avalonia.Media.TextFormatting /// /// The created text style run. /// - private static ValueSpan CreateTextStyleRun(ReadOnlySlice text, int firstTextSourceIndex, + private static ValueSpan CreateTextStyleRun(CharacterBufferRange text, int firstTextSourceIndex, TextRunProperties defaultProperties, IReadOnlyList>? textModifier) { if (textModifier == null || textModifier.Count == 0) @@ -122,7 +122,7 @@ namespace Avalonia.Media.TextFormatting return new ValueSpan(firstTextSourceIndex, length, currentProperties); } - private static int CoerceLength(ReadOnlySlice text, int length) + private static int CoerceLength(CharacterBufferRange text, int length) { var finalLength = 0; diff --git a/src/Avalonia.Base/Media/TextFormatting/InterWordJustification.cs b/src/Avalonia.Base/Media/TextFormatting/InterWordJustification.cs index a49e4ef13b..3c3a46c209 100644 --- a/src/Avalonia.Base/Media/TextFormatting/InterWordJustification.cs +++ b/src/Avalonia.Base/Media/TextFormatting/InterWordJustification.cs @@ -46,28 +46,30 @@ namespace Avalonia.Media.TextFormatting var breakOportunities = new Queue(); + 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; } } } diff --git a/src/Avalonia.Base/Media/TextFormatting/ShapeableTextCharacters.cs b/src/Avalonia.Base/Media/TextFormatting/ShapeableTextCharacters.cs index b31a6f4d13..0e8d6e3e4a 100644 --- a/src/Avalonia.Base/Media/TextFormatting/ShapeableTextCharacters.cs +++ b/src/Avalonia.Base/Media/TextFormatting/ShapeableTextCharacters.cs @@ -7,30 +7,26 @@ namespace Avalonia.Media.TextFormatting /// public sealed class ShapeableTextCharacters : TextRun { - public ShapeableTextCharacters(ReadOnlySlice 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 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; } diff --git a/src/Avalonia.Base/Media/TextFormatting/ShapedBuffer.cs b/src/Avalonia.Base/Media/TextFormatting/ShapedBuffer.cs index 85924a3d32..902b897240 100644 --- a/src/Avalonia.Base/Media/TextFormatting/ShapedBuffer.cs +++ b/src/Avalonia.Base/Media/TextFormatting/ShapedBuffer.cs @@ -7,16 +7,16 @@ namespace Avalonia.Media.TextFormatting public sealed class ShapedBuffer : IList { private static readonly IComparer s_clusterComparer = new CompareClusters(); - - public ShapedBuffer(ReadOnlySlice 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 text, ArraySlice glyphInfos, IGlyphTypeface glyphTypeface, double fontRenderingEmSize, sbyte bidiLevel) + internal ShapedBuffer(CharacterBufferRange characterBufferRange, ArraySlice 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 GlyphInfos { get; } - - public ReadOnlySlice Text { get; } - + public int Length => GlyphInfos.Length; public IGlyphTypeface GlyphTypeface { get; } @@ -45,6 +43,8 @@ namespace Avalonia.Media.TextFormatting public IReadOnlyList GlyphOffsets => new GlyphOffsetList(GlyphInfos); + public CharacterBufferRange CharacterBufferRange { get; } + /// /// Finds a glyph index for given character index. /// @@ -105,16 +105,23 @@ namespace Avalonia.Media.TextFormatting /// The split result. internal SplitResult Split(int length) { - if (Text.Length == length) + if (CharacterBufferRange.Length == length) { return new SplitResult(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(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) { diff --git a/src/Avalonia.Base/Media/TextFormatting/ShapedTextCharacters.cs b/src/Avalonia.Base/Media/TextFormatting/ShapedTextCharacters.cs index 21101f462c..3035eb7b18 100644 --- a/src/Avalonia.Base/Media/TextFormatting/ShapedTextCharacters.cs +++ b/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; } /// - public override ReadOnlySlice Text { get; } + public override CharacterBufferReference CharacterBufferReference { get; } /// public override TextRunProperties Properties { get; } /// - 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, diff --git a/src/Avalonia.Base/Media/TextFormatting/SplitResult.cs b/src/Avalonia.Base/Media/TextFormatting/SplitResult.cs index 02c7174499..53021c4656 100644 --- a/src/Avalonia.Base/Media/TextFormatting/SplitResult.cs +++ b/src/Avalonia.Base/Media/TextFormatting/SplitResult.cs @@ -1,6 +1,8 @@ namespace Avalonia.Media.TextFormatting { - internal readonly struct SplitResult +#pragma warning disable CA1815 // Override equals and operator equals on value types + public readonly struct SplitResult +#pragma warning restore CA1815 // Override equals and operator equals on value types { public SplitResult(T first, T? second) { diff --git a/src/Avalonia.Base/Media/TextFormatting/TextCharacters.cs b/src/Avalonia.Base/Media/TextFormatting/TextCharacters.cs index bcfa35ae30..0be753bd04 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextCharacters.cs +++ b/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 /// public class TextCharacters : TextRun { - public TextCharacters(ReadOnlySlice text, TextRunProperties properties) - { - TextSourceLength = text.Length; - Text = text; - Properties = properties; - } + /// + /// Construct a run of text content from character array + /// + public TextCharacters( + char[] characterArray, + int offsetToFirstChar, + int length, + TextRunProperties textRunProperties + ) : + this( + new CharacterBufferReference(characterArray, offsetToFirstChar), + length, + textRunProperties + ) + { } + - public TextCharacters(ReadOnlySlice text, int offsetToFirstCharacter, int length, - TextRunProperties properties) + /// + /// Construct a run for text content from string + /// + public TextCharacters( + string characterString, + TextRunProperties textRunProperties + ) : + this( + characterString, + 0, // offsetToFirstChar + (characterString == null) ? 0 : characterString.Length, + textRunProperties + ) + { } + + /// + /// Construct a run for text content from string + /// + public TextCharacters( + string characterString, + int offsetToFirstChar, + int length, + TextRunProperties textRunProperties + ) : + this( + new CharacterBufferReference(characterString, offsetToFirstChar), + length, + textRunProperties + ) + { } + + /// + /// Internal constructor of TextContent + /// + 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; } /// - public override int TextSourceLength { get; } + public override int Length { get; } /// - public override ReadOnlySlice Text { get; } + public override CharacterBufferReference CharacterBufferReference { get; } /// public override TextRunProperties Properties { get; } @@ -38,18 +94,17 @@ namespace Avalonia.Media.TextFormatting /// Gets a list of . /// /// The shapeable text characters. - internal IReadOnlyList GetShapeableCharacters(ReadOnlySlice runText, sbyte biDiLevel, - ref TextRunProperties? previousProperties) + internal IReadOnlyList GetShapeableCharacters(CharacterBufferRange characterBufferRange, sbyte biDiLevel, ref TextRunProperties? previousProperties) { var shapeableCharacters = new List(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 /// /// Creates a shapeable text run with unique properties. /// - /// The text to create text runs from. + /// The character buffer range to create text runs from. /// The default text run properties. /// The bidi level of the run. /// /// A list of shapeable text runs. - private static ShapeableTextCharacters CreateShapeableRun(ReadOnlySlice 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); } /// /// Tries to get a shapeable length that is supported by the specified typeface. /// - /// The text. + /// The character buffer range to shape. /// The typeface that is used to find matching characters. /// /// The shapeable length. /// /// - protected static bool TryGetShapeableLength( - ReadOnlySlice 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()) { diff --git a/src/Avalonia.Base/Media/TextFormatting/TextEllipsisHelper.cs b/src/Avalonia.Base/Media/TextFormatting/TextEllipsisHelper.cs index 5a2169630b..a1b8985b43 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextEllipsisHelper.cs +++ b/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(textRuns.Count); + var collapsedRuns = new List(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(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(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++; } diff --git a/src/Avalonia.Base/Media/TextFormatting/TextEndOfLine.cs b/src/Avalonia.Base/Media/TextFormatting/TextEndOfLine.cs index 21e354a119..ffb879e721 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextEndOfLine.cs +++ b/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; } } } diff --git a/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs b/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs index 7bad95c4a2..93eb4811b9 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs +++ b/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(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(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(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(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 ShapeTogether( - IReadOnlyList textRuns, ReadOnlySlice text, TextShaperOptions options) + IReadOnlyList textRuns, CharacterBufferReference text, int length, TextShaperOptions options) { var shapedRuns = new List(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.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(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 { 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); } diff --git a/src/Avalonia.Base/Media/TextFormatting/TextLayout.cs b/src/Avalonia.Base/Media/TextFormatting/TextLayout.cs index dc79e61333..f803001481 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextLayout.cs +++ b/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; diff --git a/src/Avalonia.Base/Media/TextFormatting/TextLeadingPrefixCharacterEllipsis.cs b/src/Avalonia.Base/Media/TextFormatting/TextLeadingPrefixCharacterEllipsis.cs index 5a14eda245..2752af8f0c 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextLeadingPrefixCharacterEllipsis.cs +++ b/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 /// width in which collapsing is constrained to /// text run properties of ellipsis symbol public TextLeadingPrefixCharacterEllipsis( - ReadOnlySlice 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!); } diff --git a/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs b/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs index 96f88d1f44..d893468052 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs @@ -56,7 +56,7 @@ namespace Avalonia.Media.TextFormatting public override double Height => _textLineMetrics.Height; /// - public override int NewLineLength => _textLineMetrics.NewLineLength; + public override int NewLineLength => _textLineMetrics.NewlineLength; /// 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; } /// @@ -528,7 +532,7 @@ namespace Avalonia.Media.TextFormatting var startX = Start; double currentWidth = 0; - var currentRect = Rect.Empty; + var currentRect = default(Rect); TextRunBounds lastRunBounds = default; @@ -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 { 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 { 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 { currentRunBounds })); - } + textBounds.Rectangle = currentRect; + + textBounds.TextRunBounds.Add(currentRunBounds); } + else + { + currentRect = currentRunBounds.Rectangle; - lastRunBounds = currentRunBounds; + result.Add(new TextBounds(currentRect, currentDirection, new List { currentRunBounds })); + } } + lastRunBounds = currentRunBounds; + currentWidth += combinedWidth; if (remainingLength <= 0 || currentPosition >= characterIndex) @@ -762,7 +748,7 @@ namespace Avalonia.Media.TextFormatting var startX = WidthIncludingTrailingWhitespace; double currentWidth = 0; - var currentRect = Rect.Empty; + var currentRect = default(Rect); for (var index = TextRuns.Count - 1; index >= 0; index--) { @@ -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; diff --git a/src/Avalonia.Base/Media/TextFormatting/TextLineMetrics.cs b/src/Avalonia.Base/Media/TextFormatting/TextLineMetrics.cs index 1799c9d3db..cb21e27696 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextLineMetrics.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextLineMetrics.cs @@ -4,15 +4,15 @@ /// Represents a metric for a objects, /// that holds information about ascent, descent, line gap, size and origin of the text line. /// - 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 @@ /// /// Gets the number of newline characters at the end of a line. /// - public int NewLineLength { get; } + public int NewlineLength { get; } /// /// Gets the distance from the start of a paragraph to the starting point of a line. diff --git a/src/Avalonia.Base/Media/TextFormatting/TextMetrics.cs b/src/Avalonia.Base/Media/TextFormatting/TextMetrics.cs index 0382e66b5a..c83ae89320 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextMetrics.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextMetrics.cs @@ -3,11 +3,11 @@ /// /// A metric that holds information about text specific measurements. /// - 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; diff --git a/src/Avalonia.Base/Media/TextFormatting/TextRange.cs b/src/Avalonia.Base/Media/TextFormatting/TextRange.cs index 1177c758f4..e8bab55aff 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextRange.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextRange.cs @@ -5,7 +5,7 @@ namespace Avalonia.Media.TextFormatting /// /// References a portion of a text buffer. /// - public readonly struct TextRange + public readonly record struct TextRange { public TextRange(int start, int length) { diff --git a/src/Avalonia.Base/Media/TextFormatting/TextRun.cs b/src/Avalonia.Base/Media/TextFormatting/TextRun.cs index 26c3f8947a..0306054767 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextRun.cs +++ b/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 /// /// Gets the text source length. /// - public virtual int TextSourceLength => DefaultTextSourceLength; + public virtual int Length => DefaultTextSourceLength; /// /// Gets the text run's text. /// - public virtual ReadOnlySlice Text => default; + public virtual CharacterBufferReference CharacterBufferReference => default; /// /// 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); } } } diff --git a/src/Avalonia.Base/Media/TextFormatting/TextRunBounds.cs b/src/Avalonia.Base/Media/TextFormatting/TextRunBounds.cs index bdc7a1ca89..707c9048dc 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextRunBounds.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextRunBounds.cs @@ -3,7 +3,7 @@ /// /// The bounding rectangle of text run /// - public readonly struct TextRunBounds + public readonly record struct TextRunBounds { /// /// Constructing TextRunBounds diff --git a/src/Avalonia.Base/Media/TextFormatting/TextShaper.cs b/src/Avalonia.Base/Media/TextFormatting/TextShaper.cs index 615b1553b6..4aacec7c48 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextShaper.cs +++ b/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 } /// - public ShapedBuffer ShapeText(ReadOnlySlice 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); } } } diff --git a/src/Avalonia.Base/Media/TextFormatting/TextShaperOptions.cs b/src/Avalonia.Base/Media/TextFormatting/TextShaperOptions.cs index 80bbbcdbfe..610fc3dbc9 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextShaperOptions.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextShaperOptions.cs @@ -5,7 +5,7 @@ namespace Avalonia.Media.TextFormatting /// /// Options to customize text shaping. /// - public readonly struct TextShaperOptions + public readonly record struct TextShaperOptions { public TextShaperOptions( IGlyphTypeface typeface, diff --git a/src/Avalonia.Base/Media/TextFormatting/TextTrailingCharacterEllipsis.cs b/src/Avalonia.Base/Media/TextFormatting/TextTrailingCharacterEllipsis.cs index 670d94e928..1de04ad061 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextTrailingCharacterEllipsis.cs +++ b/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 /// Text used as collapsing symbol. /// Width in which collapsing is constrained to. /// Text run properties of ellipsis symbol. - public TextTrailingCharacterEllipsis(ReadOnlySlice ellipsis, double width, TextRunProperties textRunProperties) + public TextTrailingCharacterEllipsis(string ellipsis, double width, TextRunProperties textRunProperties) { Width = width; Symbol = new TextCharacters(ellipsis, textRunProperties); diff --git a/src/Avalonia.Base/Media/TextFormatting/TextTrailingWordEllipsis.cs b/src/Avalonia.Base/Media/TextFormatting/TextTrailingWordEllipsis.cs index dbffbdf060..7c94715aa4 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextTrailingWordEllipsis.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextTrailingWordEllipsis.cs @@ -16,7 +16,7 @@ namespace Avalonia.Media.TextFormatting /// width in which collapsing is constrained to. /// text run properties of ellipsis symbol. public TextTrailingWordEllipsis( - ReadOnlySlice ellipsis, + string ellipsis, double width, TextRunProperties textRunProperties ) diff --git a/src/Avalonia.Base/Media/TextFormatting/Unicode/BiDiData.cs b/src/Avalonia.Base/Media/TextFormatting/Unicode/BiDiData.cs index 72df2815a9..0c51b0898d 100644 --- a/src/Avalonia.Base/Media/TextFormatting/Unicode/BiDiData.cs +++ b/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. /// /// The text to process. - public void Append(ReadOnlySlice text) + public void Append(CharacterBufferRange text) { _classes.Add(text.Length); _pairedBracketTypes.Add(text.Length); diff --git a/src/Avalonia.Base/Media/TextFormatting/Unicode/Codepoint.cs b/src/Avalonia.Base/Media/TextFormatting/Unicode/Codepoint.cs index de40839853..e6408bcfa6 100644 --- a/src/Avalonia.Base/Media/TextFormatting/Unicode/Codepoint.cs +++ b/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 /// The index to read at. /// The count of character that were read. /// - public static Codepoint ReadAt(ReadOnlySpan text, int index, out int count) + public static Codepoint ReadAt(IReadOnlyList 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; } diff --git a/src/Avalonia.Base/Media/TextFormatting/Unicode/CodepointEnumerator.cs b/src/Avalonia.Base/Media/TextFormatting/Unicode/CodepointEnumerator.cs index 9e1f748ebb..a2c36d9a13 100644 --- a/src/Avalonia.Base/Media/TextFormatting/Unicode/CodepointEnumerator.cs +++ b/src/Avalonia.Base/Media/TextFormatting/Unicode/CodepointEnumerator.cs @@ -1,12 +1,12 @@ -using Avalonia.Utilities; +using System; namespace Avalonia.Media.TextFormatting.Unicode { public ref struct CodepointEnumerator { - private ReadOnlySlice _text; + private CharacterBufferRange _text; - public CodepointEnumerator(ReadOnlySlice text) + public CodepointEnumerator(CharacterBufferRange text) { _text = text; Current = Codepoint.ReplacementCodepoint; diff --git a/src/Avalonia.Base/Media/TextFormatting/Unicode/Grapheme.cs b/src/Avalonia.Base/Media/TextFormatting/Unicode/Grapheme.cs index f268340eb9..69015fb17d 100644 --- a/src/Avalonia.Base/Media/TextFormatting/Unicode/Grapheme.cs +++ b/src/Avalonia.Base/Media/TextFormatting/Unicode/Grapheme.cs @@ -1,13 +1,13 @@ -using Avalonia.Utilities; +using System; namespace Avalonia.Media.TextFormatting.Unicode { /// /// Represents the smallest unit of a writing system of any given language. /// - public readonly struct Grapheme + public readonly ref struct Grapheme { - public Grapheme(Codepoint firstCodepoint, ReadOnlySlice text) + public Grapheme(Codepoint firstCodepoint, ReadOnlySpan text) { FirstCodepoint = firstCodepoint; Text = text; @@ -21,6 +21,6 @@ namespace Avalonia.Media.TextFormatting.Unicode /// /// The text that is representing the . /// - public ReadOnlySlice Text { get; } + public ReadOnlySpan Text { get; } } } diff --git a/src/Avalonia.Base/Media/TextFormatting/Unicode/GraphemeEnumerator.cs b/src/Avalonia.Base/Media/TextFormatting/Unicode/GraphemeEnumerator.cs index 1e4ac8fe0f..5ca120c856 100644 --- a/src/Avalonia.Base/Media/TextFormatting/Unicode/GraphemeEnumerator.cs +++ b/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 _text; + private CharacterBufferRange _text; - public GraphemeEnumerator(ReadOnlySlice 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 _buffer; + private readonly CharacterBufferRange _buffer; private int _codeUnitLengthOfCurrentScalar; - internal Processor(ReadOnlySlice buffer) + internal Processor(CharacterBufferRange buffer) { _buffer = buffer; _codeUnitLengthOfCurrentScalar = 0; diff --git a/src/Avalonia.Base/Media/TextFormatting/Unicode/LineBreak.cs b/src/Avalonia.Base/Media/TextFormatting/Unicode/LineBreak.cs index 59c4df0a2e..0e470341f3 100644 --- a/src/Avalonia.Base/Media/TextFormatting/Unicode/LineBreak.cs +++ b/src/Avalonia.Base/Media/TextFormatting/Unicode/LineBreak.cs @@ -24,7 +24,7 @@ namespace Avalonia.Media.TextFormatting.Unicode /// Information about a potential line break position /// [DebuggerDisplay("{PositionMeasure}/{PositionWrap} @ {Required}")] - public readonly struct LineBreak + public readonly record struct LineBreak { /// /// Constructor diff --git a/src/Avalonia.Base/Media/TextFormatting/Unicode/LineBreakEnumerator.cs b/src/Avalonia.Base/Media/TextFormatting/Unicode/LineBreakEnumerator.cs index e12a7c06f1..41a476c17e 100644 --- a/src/Avalonia.Base/Media/TextFormatting/Unicode/LineBreakEnumerator.cs +++ b/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 /// public ref struct LineBreakEnumerator { - private readonly ReadOnlySlice _text; + private readonly IReadOnlyList _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 text) + public LineBreakEnumerator(IReadOnlyList 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; diff --git a/src/Avalonia.Base/Media/TextHitTestResult.cs b/src/Avalonia.Base/Media/TextHitTestResult.cs index c8922f06c8..d19adc2807 100644 --- a/src/Avalonia.Base/Media/TextHitTestResult.cs +++ b/src/Avalonia.Base/Media/TextHitTestResult.cs @@ -5,7 +5,7 @@ namespace Avalonia.Media /// /// Holds a hit test result from a . /// - public readonly struct TextHitTestResult + public readonly record struct TextHitTestResult { public TextHitTestResult(CharacterHit characterHit, int textPosition, bool isInside, bool isTrailing) { diff --git a/src/Avalonia.Base/Media/TextLeadingPrefixTrimming.cs b/src/Avalonia.Base/Media/TextLeadingPrefixTrimming.cs index 19ca1a0198..7ba25eb005 100644 --- a/src/Avalonia.Base/Media/TextLeadingPrefixTrimming.cs +++ b/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 _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(ellipsis); + _ellipsis = ellipsis; } public override TextCollapsingProperties CreateCollapsingProperties(TextCollapsingCreateInfo createInfo) diff --git a/src/Avalonia.Base/Media/TextTrailingTrimming.cs b/src/Avalonia.Base/Media/TextTrailingTrimming.cs index 5bb35f0ba7..2edbaabbc6 100644 --- a/src/Avalonia.Base/Media/TextTrailingTrimming.cs +++ b/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 _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(ellipsis); + _ellipsis = ellipsis; } public override TextCollapsingProperties CreateCollapsingProperties(TextCollapsingCreateInfo createInfo) diff --git a/src/Avalonia.Base/Media/TextTrimming.cs b/src/Avalonia.Base/Media/TextTrimming.cs index e2737210be..34642c11df 100644 --- a/src/Avalonia.Base/Media/TextTrimming.cs +++ b/src/Avalonia.Base/Media/TextTrimming.cs @@ -8,7 +8,7 @@ namespace Avalonia.Media /// public abstract class TextTrimming { - internal const char DefaultEllipsisChar = '\u2026'; + internal const string DefaultEllipsisChar = "\u2026"; /// /// Text is not trimmed. diff --git a/src/Avalonia.Base/Media/Transformation/TransformOperation.cs b/src/Avalonia.Base/Media/Transformation/TransformOperation.cs index a8c0fe9b12..542d71963d 100644 --- a/src/Avalonia.Base/Media/Transformation/TransformOperation.cs +++ b/src/Avalonia.Base/Media/Transformation/TransformOperation.cs @@ -5,7 +5,7 @@ namespace Avalonia.Media.Transformation /// /// Represents a single primitive transform (like translation, rotation, scale, etc.). /// - 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; } diff --git a/src/Avalonia.Base/Media/Transformation/TransformOperations.cs b/src/Avalonia.Base/Media/Transformation/TransformOperations.cs index 334bb93562..46ca41386a 100644 --- a/src/Avalonia.Base/Media/Transformation/TransformOperations.cs +++ b/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 _operations; diff --git a/src/Avalonia.Base/Media/UnicodeRange.cs b/src/Avalonia.Base/Media/UnicodeRange.cs index 344b85bae9..4cab7b4a14 100644 --- a/src/Avalonia.Base/Media/UnicodeRange.cs +++ b/src/Avalonia.Base/Media/UnicodeRange.cs @@ -7,7 +7,7 @@ namespace Avalonia.Media /// /// The descripes a set of Unicode characters. /// - public readonly struct UnicodeRange + public readonly record struct UnicodeRange { public readonly static UnicodeRange Default = Parse("0-10FFFD"); @@ -102,9 +102,9 @@ 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})?))$"); + private static Regex s_regex = new Regex(@"^(?:[uU]\+)?(?:([0-9a-fA-F](?:[0-9a-fA-F?]{1,5})?))$", RegexOptions.Compiled); public UnicodeRangeSegment(int start, int end) { diff --git a/src/Avalonia.Base/PixelRect.cs b/src/Avalonia.Base/PixelRect.cs index 855ba104ad..469f33e7fd 100644 --- a/src/Avalonia.Base/PixelRect.cs +++ b/src/Avalonia.Base/PixelRect.cs @@ -12,6 +12,7 @@ namespace Avalonia /// /// An empty rectangle. /// + [Obsolete("Use the default keyword instead.")] public static readonly PixelRect Empty = default; /// @@ -133,9 +134,13 @@ namespace Avalonia public PixelPoint Center => new PixelPoint(X + (Width / 2), Y + (Height / 2)); /// - /// Gets a value that indicates whether the rectangle is empty. + /// Gets a value indicating whether the instance has default values (the rectangle is empty). /// - public bool IsEmpty => Width == 0 && Height == 0; + public bool IsDefault => Width == 0 && Height == 0; + + /// + [Obsolete("Use IsDefault instead.")] + public bool IsEmpty => IsDefault; /// /// Checks for equality between two s. @@ -257,7 +262,7 @@ namespace Avalonia } else { - return Empty; + return default; } } @@ -290,11 +295,11 @@ namespace Avalonia /// The union. public PixelRect Union(PixelRect rect) { - if (IsEmpty) + if (IsDefault) { return rect; } - else if (rect.IsEmpty) + else if (rect.IsDefault) { return this; } diff --git a/src/Avalonia.Base/Platform/IOptionalFeatureProvider.cs b/src/Avalonia.Base/Platform/IOptionalFeatureProvider.cs new file mode 100644 index 0000000000..b6464dea58 --- /dev/null +++ b/src/Avalonia.Base/Platform/IOptionalFeatureProvider.cs @@ -0,0 +1,18 @@ +using System; + +namespace Avalonia.Platform; + +public interface IOptionalFeatureProvider +{ + /// + /// Queries for an optional feature + /// + /// Feature type + public object? TryGetFeature(Type featureType); +} + +public static class OptionalFeatureProviderExtensions +{ + public static T? TryGetFeature(this IOptionalFeatureProvider provider) where T : class => + (T?)provider.TryGetFeature(typeof(T)); +} \ No newline at end of file diff --git a/src/Avalonia.Base/Platform/IPlatformGpu.cs b/src/Avalonia.Base/Platform/IPlatformGpu.cs index 0507dea1d7..f6953619d2 100644 --- a/src/Avalonia.Base/Platform/IPlatformGpu.cs +++ b/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 +{ + } \ No newline at end of file diff --git a/src/Avalonia.Base/Platform/IPlatformRenderInterface.cs b/src/Avalonia.Base/Platform/IPlatformRenderInterface.cs index 518c5f37b8..1828f24aff 100644 --- a/src/Avalonia.Base/Platform/IPlatformRenderInterface.cs +++ b/src/Avalonia.Base/Platform/IPlatformRenderInterface.cs @@ -65,15 +65,6 @@ namespace Avalonia.Platform /// The geometry returned contains the combined geometry of all glyphs in the glyph run. IGeometryImpl BuildGlyphRunGeometry(GlyphRun glyphRun); - /// - /// Creates a renderer. - /// - /// - /// The list of native platform surfaces that can be used for output. - /// - /// An . - IRenderTarget CreateRenderTarget(IEnumerable surfaces); - /// /// Creates a render target bitmap implementation. /// @@ -181,6 +172,13 @@ namespace Avalonia.Platform /// IGlyphRunImpl CreateGlyphRun(IGlyphTypeface glyphTypeface, double fontRenderingEmSize, IReadOnlyList glyphIndices, IReadOnlyList? glyphAdvances, IReadOnlyList? glyphOffsets); + /// + /// Creates a backend-specific object using a low-level API graphics context + /// + /// An underlying low-level graphics context (e. g. wrapped OpenGL context, Vulkan device, D3DDevice, etc) + /// + IPlatformRenderInterfaceContext CreateBackendContext(IPlatformGraphicsContext? graphicsApiContext); + /// /// Gets a value indicating whether the platform directly supports rectangles with rounded corners. /// @@ -200,4 +198,17 @@ namespace Avalonia.Platform /// public PixelFormat DefaultPixelFormat { get; } } + + [Unstable] + public interface IPlatformRenderInterfaceContext : IOptionalFeatureProvider, IDisposable + { + /// + /// Creates a renderer. + /// + /// + /// The list of native platform surfaces that can be used for output. + /// + /// An . + IRenderTarget CreateRenderTarget(IEnumerable surfaces); + } } diff --git a/src/Avalonia.Base/Platform/IRenderTarget.cs b/src/Avalonia.Base/Platform/IRenderTarget.cs index 7023f2ca51..73e9e58da4 100644 --- a/src/Avalonia.Base/Platform/IRenderTarget.cs +++ b/src/Avalonia.Base/Platform/IRenderTarget.cs @@ -19,10 +19,10 @@ namespace Avalonia.Platform /// to be drawn. /// IDrawingContextImpl CreateDrawingContext(IVisualBrushRenderer? visualBrushRenderer); - } - - public interface IRenderTargetWithCorruptionInfo : IRenderTarget - { - bool IsCorrupted { get; } + + /// + /// Indicates if the render target is no longer usable and needs to be recreated + /// + public bool IsCorrupted { get; } } } diff --git a/src/Avalonia.Base/Platform/IRuntimePlatform.cs b/src/Avalonia.Base/Platform/IRuntimePlatform.cs index 3f8983479f..91d2a1e0cf 100644 --- a/src/Avalonia.Base/Platform/IRuntimePlatform.cs +++ b/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; } diff --git a/src/Avalonia.Base/Platform/ITextShaperImpl.cs b/src/Avalonia.Base/Platform/ITextShaperImpl.cs index 10e58b7d0b..c3eb89ab1a 100644 --- a/src/Avalonia.Base/Platform/ITextShaperImpl.cs +++ b/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,10 @@ namespace Avalonia.Platform /// /// Shapes the specified region within the text and returns a shaped buffer. /// - /// The text. + /// The text buffer. + /// The length of text. /// Text shaper options to customize the shaping process. /// A shaped glyph run. - ShapedBuffer ShapeText(ReadOnlySlice text, TextShaperOptions options); + ShapedBuffer ShapeText(CharacterBufferReference text, int length, TextShaperOptions options); } } diff --git a/src/Avalonia.Base/Rect.cs b/src/Avalonia.Base/Rect.cs index a91b089a33..831ab28adc 100644 --- a/src/Avalonia.Base/Rect.cs +++ b/src/Avalonia.Base/Rect.cs @@ -18,7 +18,8 @@ namespace Avalonia /// /// An empty rectangle. /// - public static readonly Rect Empty = default(Rect); + [Obsolete("Use the default keyword instead.")] + public static readonly Rect Empty = default; /// /// The X position. @@ -169,12 +170,16 @@ namespace Avalonia public Point Center => new Point(_x + (_width / 2), _y + (_height / 2)); /// - /// Gets a value that indicates whether the rectangle is empty. + /// Gets a value indicating whether the instance has default values (the rectangle is empty). /// // ReSharper disable CompareOfFloatsByEqualityOperator - public bool IsEmpty => _width == 0 && _height == 0; + public bool IsDefault => _width == 0 && _height == 0; // ReSharper restore CompareOfFloatsByEqualityOperator + /// + [Obsolete("Use IsDefault instead.")] + public bool IsEmpty => IsDefault; + /// /// Checks for equality between two s. /// @@ -390,7 +395,7 @@ namespace Avalonia } else { - return Empty; + return default; } } @@ -457,13 +462,13 @@ namespace Avalonia /// public Rect Normalize() { - Rect rect = this; + Rect rect = this; if(double.IsNaN(rect.Right) || double.IsNaN(rect.Bottom) || double.IsNaN(rect.X) || double.IsNaN(rect.Y) || double.IsNaN(Height) || double.IsNaN(Width)) { - return Rect.Empty; + return default; } if (rect.Width < 0) @@ -493,11 +498,11 @@ namespace Avalonia /// The union. public Rect Union(Rect rect) { - if (IsEmpty) + if (IsDefault) { return rect; } - else if (rect.IsEmpty) + else if (rect.IsDefault) { return this; } diff --git a/src/Avalonia.Base/Rendering/Composition/Animations/KeyFrameAnimationInstance.cs b/src/Avalonia.Base/Rendering/Composition/Animations/KeyFrameAnimationInstance.cs index e9f58be095..986110b5e6 100644 --- a/src/Avalonia.Base/Rendering/Composition/Animations/KeyFrameAnimationInstance.cs +++ b/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(); } diff --git a/src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs b/src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs index ea8a408e6f..b2080aeb87 100644 --- a/src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs +++ b/src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs @@ -38,13 +38,12 @@ public class CompositingRenderer : IRendererWithCompositor /// public bool RenderOnlyOnRenderThread { get; set; } = true; - public CompositingRenderer(IRenderRoot root, - Compositor compositor) + public CompositingRenderer(IRenderRoot root, Compositor compositor, Func> 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 TryGetRenderInterfaceFeature(Type featureType) => Compositor.TryGetRenderInterfaceFeature(featureType); + public void Dispose() { Stop(); diff --git a/src/Avalonia.Base/Rendering/Composition/Compositor.Factories.cs b/src/Avalonia.Base/Rendering/Composition/Compositor.Factories.cs index 00fa7b3315..4ce87b67a5 100644 --- a/src/Avalonia.Base/Rendering/Composition/Compositor.Factories.cs +++ b/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 /// /// Creates a new CompositionTarget /// - /// A factory method to create IRenderTarget to be called from the render thread + /// A factory method to create IRenderTarget to be called from the render thread /// - public CompositionTarget CreateCompositionTarget(Func renderTargetFactory) + public CompositionTarget CreateCompositionTarget(Func> 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)); diff --git a/src/Avalonia.Base/Rendering/Composition/Compositor.cs b/src/Avalonia.Base/Rendering/Composition/Compositor.cs index e6b9600e45..b4817bfe9a 100644 --- a/src/Avalonia.Base/Rendering/Composition/Compositor.cs +++ b/src/Avalonia.Base/Rendering/Composition/Compositor.cs @@ -34,6 +34,7 @@ namespace Avalonia.Rendering.Composition internal ServerCompositor Server => _server; private Task? _pendingBatch; private readonly object _pendingBatchLock = new(); + private List _pendingServerCompositorJobs = new(); internal IEasing DefaultEasing { get; } @@ -43,7 +44,7 @@ namespace Avalonia.Rendering.Composition /// /// /// - public Compositor(IRenderLoop loop, IPlatformGpu? gpu) + public Compositor(IRenderLoop loop, IPlatformGraphics? gpu) { Loop = loop; _server = new ServerCompositor(loop, gpu, _batchObjectPool, _batchMemoryPool); @@ -101,6 +102,13 @@ namespace Avalonia.Rendering.Composition #endif } _objectsForSerialization.Clear(); + if (_pendingServerCompositorJobs.Count > 0) + { + writer.WriteObject(ServerCompositor.RenderThreadJobsStartMarker); + foreach (var job in _pendingServerCompositorJobs) + writer.WriteObject(job); + writer.WriteObject(ServerCompositor.RenderThreadJobsEndMarker); + } } batch.CommitedAt = Server.Clock.Elapsed; @@ -136,5 +144,28 @@ namespace Avalonia.Rendering.Composition _invokeBeforeCommit.Enqueue(action); RequestCommitAsync(); } + + /// + /// Attempts to query for a feature from the platform render interface + /// + public ValueTask TryGetRenderInterfaceFeature(Type featureType) + { + var tcs = new TaskCompletionSource(); + _pendingServerCompositorJobs.Add(() => + { + try + { + using (Server.RenderInterface.EnsureCurrent()) + { + tcs.TrySetResult(Server.RenderInterface.Value.TryGetFeature(featureType)); + } + } + catch (Exception e) + { + tcs.TrySetException(e); + } + }); + return new ValueTask(tcs.Task); + } } } diff --git a/src/Avalonia.Base/Rendering/Composition/Server/DrawingContextProxy.cs b/src/Avalonia.Base/Rendering/Composition/Server/DrawingContextProxy.cs index 03859d241f..e6bbba6ec0 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/DrawingContextProxy.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/DrawingContextProxy.cs @@ -163,7 +163,7 @@ internal class CompositorDrawingContextProxy : IDrawingContextImpl, IDrawingCont public CompositionDrawList? VisualBrushDrawList { get; set; } public Size GetRenderTargetSize(IVisualBrush brush) { - return VisualBrushDrawList?.Size ?? Size.Empty; + return VisualBrushDrawList?.Size ?? default; } public void RenderVisualBrush(IDrawingContextImpl context, IVisualBrush brush) diff --git a/src/Avalonia.Base/Rendering/Composition/Server/FpsCounter.cs b/src/Avalonia.Base/Rendering/Composition/Server/FpsCounter.cs index 2019ad6faa..18cb7a6308 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/FpsCounter.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/FpsCounter.cs @@ -1,6 +1,7 @@ using System; using System.Diagnostics; using System.Globalization; +using System.Linq; using Avalonia.Media; using Avalonia.Media.TextFormatting; using Avalonia.Platform; @@ -31,7 +32,7 @@ internal class FpsCounter { var s = new string((char)c, 1); var glyph = typeface.GetGlyph((uint)(s[0])); - _runs[c - FirstChar] = new GlyphRun(typeface, 18, new ReadOnlySlice(s.AsMemory()), new ushort[] { glyph }); + _runs[c - FirstChar] = new GlyphRun(typeface, 18, s.ToArray(), new ushort[] { glyph }); } } diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionDrawListVisual.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionDrawListVisual.cs index 6cbd2797f9..8dc088fed1 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionDrawListVisual.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionDrawListVisual.cs @@ -37,7 +37,7 @@ internal class ServerCompositionDrawListVisual : ServerCompositionContainerVisua { if (_contentBounds == null) { - var rect = Rect.Empty; + var rect = default(Rect); if(_renderCommands!=null) foreach (var cmd in _renderCommands) rect = rect.Union(cmd.Item.Bounds); diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs index f5a46506a3..0fc5cecef3 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs @@ -20,7 +20,7 @@ namespace Avalonia.Rendering.Composition.Server internal partial class ServerCompositionTarget : IDisposable { private readonly ServerCompositor _compositor; - private readonly Func _renderTargetFactory; + private readonly Func> _surfaces; private static long s_nextId = 1; public long Id { get; } public ulong Revision { get; private set; } @@ -39,11 +39,11 @@ namespace Avalonia.Rendering.Composition.Server public ReadbackIndices Readback { get; } = new(); public int RenderedVisuals { get; set; } - public ServerCompositionTarget(ServerCompositor compositor, Func renderTargetFactory) : + public ServerCompositionTarget(ServerCompositor compositor, Func> surfaces) : base(compositor) { _compositor = compositor; - _renderTargetFactory = renderTargetFactory; + _surfaces = surfaces; Id = Interlocked.Increment(ref s_nextId); } @@ -79,17 +79,18 @@ namespace Avalonia.Rendering.Composition.Server if (Root == null) return; - if ((_renderTarget as IRenderTargetWithCorruptionInfo)?.IsCorrupted == true) + if (_renderTarget?.IsCorrupted == true) { _renderTarget!.Dispose(); _renderTarget = null; + _redrawRequested = true; } - _renderTarget ??= _renderTargetFactory(); + _renderTarget ??= _compositor.CreateRenderTarget(_surfaces()); Compositor.UpdateServerTime(); - if(_dirtyRect.IsEmpty && !_redrawRequested) + if(_dirtyRect.IsDefault && !_redrawRequested) return; Revision++; @@ -109,15 +110,16 @@ namespace Avalonia.Rendering.Composition.Server using (var targetContext = _renderTarget.CreateDrawingContext(null)) { var layerSize = Size * Scaling; - if (layerSize != _layerSize || _layer == null) + if (layerSize != _layerSize || _layer == null || _layer.IsCorrupted) { _layer?.Dispose(); _layer = null; _layer = targetContext.CreateLayer(Size); _layerSize = layerSize; + _dirtyRect = new Rect(0, 0, layerSize.Width, layerSize.Height); } - if (!_dirtyRect.IsEmpty) + if (!_dirtyRect.IsDefault) { var visualBrushHelper = new CompositorDrawingContextProxy.VisualBrushRenderer(); using (var context = _layer.CreateDrawingContext(visualBrushHelper)) @@ -160,7 +162,7 @@ namespace Avalonia.Rendering.Composition.Server } RenderedVisuals = 0; - _dirtyRect = Rect.Empty; + _dirtyRect = default; } } @@ -179,7 +181,7 @@ namespace Avalonia.Rendering.Composition.Server public void AddDirtyRect(Rect rect) { - if(rect.IsEmpty) + if(rect.IsDefault) return; var snapped = SnapToDevicePixels(rect, Scaling); DebugEvents?.RectInvalidated(rect); @@ -197,7 +199,7 @@ namespace Avalonia.Rendering.Composition.Server if(_disposed) return; _disposed = true; - using (_compositor.GpuContext?.EnsureCurrent()) + using (_compositor.RenderInterface.EnsureCurrent()) { if (_layer != null) { diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual.cs index d724e14298..387998f8d6 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual.cs @@ -37,7 +37,7 @@ namespace Avalonia.Rendering.Composition.Server return; currentTransformedClip = currentTransformedClip.Intersect(_combinedTransformedClipBounds); - if(currentTransformedClip.IsEmpty) + if(currentTransformedClip.IsDefault) return; Root!.RenderedVisuals++; @@ -139,7 +139,7 @@ namespace Avalonia.Rendering.Composition.Server if (ownBounds != _oldOwnContentBounds || positionChanged) { _oldOwnContentBounds = ownBounds; - if (ownBounds.IsEmpty) + if (ownBounds.IsDefault) TransformedOwnContentBounds = default; else TransformedOwnContentBounds = @@ -163,7 +163,7 @@ namespace Avalonia.Rendering.Composition.Server EffectiveOpacity = Opacity * (Parent?.EffectiveOpacity ?? 1); IsVisibleInFrame = _parent?.IsVisibleInFrame != false && Visible && EffectiveOpacity > 0.04 && !_isBackface && - !_combinedTransformedClipBounds.IsEmpty; + !_combinedTransformedClipBounds.IsDefault; if (wasVisible != IsVisibleInFrame || positionChanged) { @@ -192,7 +192,7 @@ namespace Avalonia.Rendering.Composition.Server void AddDirtyRect(Rect rc) { - if(rc == Rect.Empty) + if(rc == default) return; Root?.AddDirtyRect(rc); } diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositor.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositor.cs index bfc2b2d626..041a3dd6af 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositor.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositor.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Diagnostics; +using Avalonia.Logging; using Avalonia.Platform; using Avalonia.Rendering.Composition.Animations; using Avalonia.Rendering.Composition.Expressions; @@ -19,6 +20,7 @@ namespace Avalonia.Rendering.Composition.Server internal class ServerCompositor : IRenderLoopTask { private readonly IRenderLoop _renderLoop; + private readonly Queue _batches = new Queue(); public long LastBatchId { get; private set; } public Stopwatch Clock { get; } = Stopwatch.StartNew(); @@ -29,13 +31,15 @@ namespace Avalonia.Rendering.Composition.Server internal BatchStreamObjectPool BatchObjectPool; internal BatchStreamMemoryPool BatchMemoryPool; private object _lock = new object(); - public IPlatformGpuContext? GpuContext { get; } + public PlatformRenderInterfaceContextManager RenderInterface { get; } + internal static readonly object RenderThreadJobsStartMarker = new(); + internal static readonly object RenderThreadJobsEndMarker = new(); - public ServerCompositor(IRenderLoop renderLoop, IPlatformGpu? platformGpu, + public ServerCompositor(IRenderLoop renderLoop, IPlatformGraphics? platformGraphics, BatchStreamObjectPool batchObjectPool, BatchStreamMemoryPool batchMemoryPool) { - GpuContext = platformGpu?.PrimaryContext; _renderLoop = renderLoop; + RenderInterface = new PlatformRenderInterfaceContextManager(platformGraphics); BatchObjectPool = batchObjectPool; BatchMemoryPool = batchMemoryPool; _renderLoop.Add(this); @@ -66,7 +70,14 @@ namespace Avalonia.Rendering.Composition.Server { while (!stream.IsObjectEof) { - var target = (ServerObject)stream.ReadObject()!; + var readObject = stream.ReadObject(); + if (readObject == RenderThreadJobsStartMarker) + { + ReadAndExecuteJobs(stream); + continue; + } + + var target = (ServerObject)readObject!; target.DeserializeChanges(stream, batch); #if DEBUG_COMPOSITOR_SERIALIZATION if (stream.ReadObject() != BatchStreamDebugMarkers.ObjectEndMarker) @@ -84,6 +95,23 @@ namespace Avalonia.Rendering.Composition.Server } } + void ReadAndExecuteJobs(BatchStreamReader reader) + { + object? readObject; + while ((readObject = reader.ReadObject()) != RenderThreadJobsEndMarker) + { + var job = (Action)readObject!; + try + { + job(); + } + catch + { + // Ignore + } + } + } + void CompletePendingBatches() { foreach(var batch in _reusableToCompleteList) @@ -118,8 +146,16 @@ namespace Avalonia.Rendering.Composition.Server _animationsToUpdate.Clear(); - foreach (var t in _activeTargets) - t.Render(); + try + { + RenderInterface.EnsureValidBackendContext(); + foreach (var t in _activeTargets) + t.Render(); + } + catch (Exception e) + { + Logger.TryGet(LogEventLevel.Error, LogArea.Visual)?.Log(this, "Exception when rendering: {Error}", e); + } } public void AddCompositionTarget(ServerCompositionTarget target) @@ -137,5 +173,11 @@ namespace Avalonia.Rendering.Composition.Server public void RemoveFromClock(IAnimationInstance animationInstance) => _activeAnimations.Remove(animationInstance); + + public IRenderTarget CreateRenderTarget(IEnumerable surfaces) + { + using (RenderInterface.EnsureCurrent()) + return RenderInterface.CreateRenderTarget(surfaces); + } } } diff --git a/src/Avalonia.Base/Rendering/Composition/Transport/BatchStream.cs b/src/Avalonia.Base/Rendering/Composition/Transport/BatchStream.cs index 8b68900994..a3cad3cebd 100644 --- a/src/Avalonia.Base/Rendering/Composition/Transport/BatchStream.cs +++ b/src/Avalonia.Base/Rendering/Composition/Transport/BatchStream.cs @@ -21,7 +21,7 @@ internal class BatchStreamData public Queue> Structs { get; } = new(); } -public struct BatchStreamSegment +public record struct BatchStreamSegment { public TData Data { get; set; } public int ElementCount { get; set; } diff --git a/src/Avalonia.Base/Rendering/DeferredRenderer.cs b/src/Avalonia.Base/Rendering/DeferredRenderer.cs index c3bf861c47..f0b993a2b0 100644 --- a/src/Avalonia.Base/Rendering/DeferredRenderer.cs +++ b/src/Avalonia.Base/Rendering/DeferredRenderer.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; +using System.Threading.Tasks; using Avalonia.Logging; using Avalonia.Media; using Avalonia.Media.Immutable; @@ -22,6 +23,8 @@ namespace Avalonia.Rendering { private readonly IDispatcher? _dispatcher; private readonly IRenderLoop? _renderLoop; + private readonly Func? _renderTargetFactory; + private readonly PlatformRenderInterfaceContextManager? _renderInterface; private readonly Visual _root; private readonly ISceneBuilder _sceneBuilder; @@ -39,18 +42,23 @@ namespace Avalonia.Rendering private readonly object _startStopLock = new object(); private readonly object _renderLoopIsRenderingLock = new object(); private readonly Action _updateSceneIfNeededDelegate; + private List? _pendingRenderThreadJobs; /// /// Initializes a new instance of the class. /// /// The control to render. /// The render loop. + /// The target render factory. + /// The Platform Render Context. /// The scene builder to use. Optional. /// The dispatcher to use. Optional. /// Lock object used before trying to access render target public DeferredRenderer( IRenderRoot root, IRenderLoop renderLoop, + Func renderTargetFactory, + PlatformRenderInterfaceContextManager? renderInterface = null, ISceneBuilder? sceneBuilder = null, IDispatcher? dispatcher = null, IDeferredRendererLock? rendererLock = null) : base(true) @@ -60,6 +68,8 @@ namespace Avalonia.Rendering _sceneBuilder = sceneBuilder ?? new SceneBuilder(); Layers = new RenderLayers(); _renderLoop = renderLoop; + _renderTargetFactory = renderTargetFactory; + _renderInterface = renderInterface; _lock = rendererLock ?? new ManagedDeferredRendererLock(); _updateSceneIfNeededDelegate = UpdateSceneIfNeeded; } @@ -256,6 +266,30 @@ namespace Avalonia.Rendering } } + public ValueTask TryGetRenderInterfaceFeature(Type featureType) + { + if (_renderInterface == null) + return new((object?)null); + + var tcs = new TaskCompletionSource(); + _pendingRenderThreadJobs ??= new(); + _pendingRenderThreadJobs.Add(() => + { + try + { + using (_renderInterface.EnsureCurrent()) + { + tcs.TrySetResult(_renderInterface.Value.TryGetFeature(featureType)); + } + } + catch (Exception e) + { + tcs.TrySetException(e); + } + }); + return new ValueTask(tcs.Task); + } + bool NeedsUpdate => _dirty == null || _dirty.Count > 0; bool IRenderLoopTask.NeedsUpdate => NeedsUpdate; @@ -277,7 +311,7 @@ namespace Avalonia.Rendering /// Size IVisualBrushRenderer.GetRenderTargetSize(IVisualBrush brush) { - return TryGetChildScene(_currentDraw)?.Size ?? Size.Empty; + return TryGetChildScene(_currentDraw)?.Size ?? default; } /// @@ -337,7 +371,16 @@ namespace Avalonia.Rendering } finally { - scene.Item.MarkAsRendered(); + try + { + if(scene.Item.RenderThreadJobs!=null) + foreach (var job in scene.Item.RenderThreadJobs) + job(); + } + finally + { + scene.Item.MarkAsRendered(); + } } } } @@ -420,7 +463,7 @@ namespace Avalonia.Rendering { clipBounds = node.ClipBounds.Intersect(clipBounds); - if (!clipBounds.IsEmpty && node.Opacity > 0) + if (!clipBounds.IsDefault && node.Opacity > 0) { var isLayerRoot = node.Visual == layer; @@ -604,7 +647,7 @@ namespace Avalonia.Rendering return; } - if ((RenderTarget as IRenderTargetWithCorruptionInfo)?.IsCorrupted == true) + if (RenderTarget?.IsCorrupted == true) { RenderTarget!.Dispose(); RenderTarget = null; @@ -612,7 +655,7 @@ namespace Avalonia.Rendering if (RenderTarget == null) { - RenderTarget = ((IRenderRoot)_root).CreateRenderTarget(); + RenderTarget = _renderTargetFactory!(); } context = RenderTarget.CreateDrawingContext(this); @@ -637,7 +680,11 @@ namespace Avalonia.Rendering } if (_root.IsVisible) { - var sceneRef = RefCountable.Create(_scene?.Item.CloneScene() ?? new Scene(_root)); + var sceneRef = RefCountable.Create(_scene?.Item.CloneScene() ?? new Scene(_root) + { + RenderThreadJobs = _pendingRenderThreadJobs + }); + _pendingRenderThreadJobs = null; var scene = sceneRef.Item; if (_dirty == null) diff --git a/src/Avalonia.Base/Rendering/DirtyRects.cs b/src/Avalonia.Base/Rendering/DirtyRects.cs index f4b6a6b6ce..723fe400b3 100644 --- a/src/Avalonia.Base/Rendering/DirtyRects.cs +++ b/src/Avalonia.Base/Rendering/DirtyRects.cs @@ -10,6 +10,9 @@ namespace Avalonia.Rendering { private List _rects = new List(); + /// + /// Gets a value indicating whether the collection of dirty rectangles is empty. + /// public bool IsEmpty => _rects.Count == 0; /// @@ -27,7 +30,7 @@ namespace Avalonia.Rendering /// public void Add(Rect rect) { - if (!rect.IsEmpty) + if (!rect.IsDefault) { for (var i = 0; i < _rects.Count; ++i) { diff --git a/src/Avalonia.Base/Rendering/IRenderRoot.cs b/src/Avalonia.Base/Rendering/IRenderRoot.cs index aa06ce7bed..cabb1302cd 100644 --- a/src/Avalonia.Base/Rendering/IRenderRoot.cs +++ b/src/Avalonia.Base/Rendering/IRenderRoot.cs @@ -25,12 +25,6 @@ namespace Avalonia.Rendering /// double RenderScaling { get; } - /// - /// Creates a render target for the window. - /// - /// An . - IRenderTarget CreateRenderTarget(); - /// /// Adds a rectangle to the window's dirty region. /// diff --git a/src/Avalonia.Base/Rendering/IRenderer.cs b/src/Avalonia.Base/Rendering/IRenderer.cs index cb36aad7cc..f3f5b5e99b 100644 --- a/src/Avalonia.Base/Rendering/IRenderer.cs +++ b/src/Avalonia.Base/Rendering/IRenderer.cs @@ -1,6 +1,7 @@ using System; using Avalonia.VisualTree; using System.Collections.Generic; +using System.Threading.Tasks; using Avalonia.Rendering.Composition; namespace Avalonia.Rendering @@ -87,6 +88,11 @@ namespace Avalonia.Rendering /// Stops the renderer. /// void Stop(); + + /// + /// Attempts to query for a feature from the platform render interface + /// + public ValueTask TryGetRenderInterfaceFeature(Type featureType); } public interface IRendererWithCompositor : IRenderer diff --git a/src/Avalonia.Base/Rendering/ImmediateRenderer.cs b/src/Avalonia.Base/Rendering/ImmediateRenderer.cs index 9e1582ed43..4909c78ed1 100644 --- a/src/Avalonia.Base/Rendering/ImmediateRenderer.cs +++ b/src/Avalonia.Base/Rendering/ImmediateRenderer.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Threading.Tasks; using Avalonia.Logging; using Avalonia.Media; using Avalonia.Platform; @@ -19,6 +20,8 @@ namespace Avalonia.Rendering public class ImmediateRenderer : RendererBase, IRenderer, IVisualBrushRenderer { private readonly Visual _root; + private readonly Func _renderTargetFactory; + private readonly PlatformRenderInterfaceContextManager? _renderContext; private readonly IRenderRoot? _renderRoot; private bool _updateTransformedBounds = true; private IRenderTarget? _renderTarget; @@ -27,15 +30,21 @@ namespace Avalonia.Rendering /// Initializes a new instance of the class. /// /// The control to render. - public ImmediateRenderer(Visual root) + /// The target render factory. + /// The render contex. + public ImmediateRenderer(Visual root, Func renderTargetFactory, + PlatformRenderInterfaceContextManager? renderContext = null) { _root = root ?? throw new ArgumentNullException(nameof(root)); + _renderTargetFactory = renderTargetFactory; + _renderContext = renderContext; _renderRoot = root as IRenderRoot; } - private ImmediateRenderer(Visual root, bool updateTransformedBounds) + private ImmediateRenderer(Visual root, Func renderTargetFactory, bool updateTransformedBounds) { _root = root ?? throw new ArgumentNullException(nameof(root)); + _renderTargetFactory = renderTargetFactory; _renderRoot = root as IRenderRoot; _updateTransformedBounds = updateTransformedBounds; } @@ -54,7 +63,7 @@ namespace Avalonia.Rendering { if (_renderTarget == null) { - _renderTarget = ((IRenderRoot)_root).CreateRenderTarget(); + _renderTarget = _renderTargetFactory(); } try @@ -104,7 +113,7 @@ namespace Avalonia.Rendering /// The render target. public static void Render(Visual visual, IRenderTarget target) { - using (var renderer = new ImmediateRenderer(visual, updateTransformedBounds: false)) + using (var renderer = new ImmediateRenderer(visual, () => target, updateTransformedBounds: false)) using (var context = new DrawingContext(target.CreateDrawingContext(renderer))) { renderer.Render(context, visual, visual.Bounds); @@ -118,7 +127,9 @@ namespace Avalonia.Rendering /// The drawing context. public static void Render(Visual visual, DrawingContext context) { - using (var renderer = new ImmediateRenderer(visual, updateTransformedBounds: false)) + using (var renderer = new ImmediateRenderer(visual, + () => throw new InvalidOperationException("This is not supposed to be called"), + updateTransformedBounds: false)) { renderer.Render(context, visual, visual.Bounds); } @@ -127,7 +138,7 @@ namespace Avalonia.Rendering /// public void AddDirty(Visual visual) { - if (visual.Bounds != Rect.Empty) + if (!visual.Bounds.IsDefault) { var m = visual.TransformToVisual(_root); @@ -185,11 +196,14 @@ namespace Avalonia.Rendering { } + public ValueTask TryGetRenderInterfaceFeature(Type featureType) => + new(_renderContext?.Value?.TryGetFeature(featureType)); + /// Size IVisualBrushRenderer.GetRenderTargetSize(IVisualBrush brush) { (brush.Visual as IVisualBrushInitialize)?.EnsureInitialized(); - return brush.Visual?.Bounds.Size ?? Size.Empty; + return brush.Visual?.Bounds.Size ?? default; } /// @@ -201,7 +215,9 @@ namespace Avalonia.Rendering internal static void Render(Visual visual, DrawingContext context, bool updateTransformedBounds) { - using var renderer = new ImmediateRenderer(visual, updateTransformedBounds); + using var renderer = new ImmediateRenderer(visual, + () => throw new InvalidOperationException("This is not supposed to be called"), + updateTransformedBounds); renderer.Render(context, visual, visual.Bounds); } diff --git a/src/Avalonia.Base/Rendering/OwnedDisposable.cs b/src/Avalonia.Base/Rendering/OwnedDisposable.cs new file mode 100644 index 0000000000..27122751aa --- /dev/null +++ b/src/Avalonia.Base/Rendering/OwnedDisposable.cs @@ -0,0 +1,24 @@ +using System; + +namespace Avalonia.Rendering; + +struct OwnedDisposable :IDisposable where T : class, IDisposable +{ + private readonly bool _owns; + private T? _value; + + public T Value => _value ?? throw new ObjectDisposedException("OwnedDisposable"); + + public OwnedDisposable(T value, bool owns) + { + _owns = owns; + _value = value; + } + + public void Dispose() + { + if(_owns) + _value?.Dispose(); + _value = null; + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/PlatformRenderInterfaceContextManager.cs b/src/Avalonia.Base/Rendering/PlatformRenderInterfaceContextManager.cs new file mode 100644 index 0000000000..198b36564a --- /dev/null +++ b/src/Avalonia.Base/Rendering/PlatformRenderInterfaceContextManager.cs @@ -0,0 +1,66 @@ +using System; +using System.Collections.Generic; +using System.Reactive.Disposables; +using Avalonia.Metadata; +using Avalonia.Platform; + +namespace Avalonia.Rendering; + +[Unstable] +// TODO: Make it internal once legacy renderers are removed +public class PlatformRenderInterfaceContextManager +{ + private readonly IPlatformGraphics? _graphics; + private IPlatformRenderInterfaceContext? _backend; + private OwnedDisposable? _gpuContext; + + public PlatformRenderInterfaceContextManager(IPlatformGraphics? graphics) + { + _graphics = graphics; + } + + public void EnsureValidBackendContext() + { + if (_backend == null || _gpuContext?.Value.IsLost == true) + { + _backend?.Dispose(); + _backend = null; + _gpuContext?.Dispose(); + _gpuContext = null; + + if (_graphics != null) + { + if (_graphics.UsesSharedContext) + _gpuContext = new OwnedDisposable(_graphics.GetSharedContext(), false); + else + _gpuContext = new OwnedDisposable(_graphics.CreateContext(), true); + } + + _backend = AvaloniaLocator.Current.GetRequiredService() + .CreateBackendContext(_gpuContext?.Value); + } + } + + public IPlatformRenderInterfaceContext Value + { + get + { + EnsureValidBackendContext(); + return _backend!; + } + } + + public IDisposable EnsureCurrent() + { + EnsureValidBackendContext(); + if (_gpuContext.HasValue) + return _gpuContext.Value.Value.EnsureCurrent(); + return Disposable.Empty; + } + + public IRenderTarget CreateRenderTarget(IEnumerable surfaces) + { + EnsureValidBackendContext(); + return _backend!.CreateRenderTarget(surfaces); + } +} diff --git a/src/Avalonia.Base/Rendering/RenderLoop.cs b/src/Avalonia.Base/Rendering/RenderLoop.cs index c66fec92aa..5a08bfc6a1 100644 --- a/src/Avalonia.Base/Rendering/RenderLoop.cs +++ b/src/Avalonia.Base/Rendering/RenderLoop.cs @@ -19,6 +19,7 @@ namespace Avalonia.Rendering private readonly IDispatcher _dispatcher; private List _items = new List(); private List _itemsCopy = new List(); + private List _updateItemsCopy = new List(); private IRenderTimer? _timer; private int _inTick; private int _inUpdate; @@ -97,7 +98,13 @@ namespace Avalonia.Rendering { bool needsUpdate = false; - foreach (IRenderLoopTask item in _items) + lock (_items) + { + _itemsCopy.Clear(); + _itemsCopy.AddRange(_items); + } + + foreach (IRenderLoopTask item in _itemsCopy) { if (item.NeedsUpdate) { @@ -112,10 +119,13 @@ namespace Avalonia.Rendering { _dispatcher.Post(() => { - for (var i = 0; i < _items.Count; ++i) + lock (_items) + { + _updateItemsCopy.Clear(); + _updateItemsCopy.AddRange(_items); + } + foreach (var item in _updateItemsCopy) { - var item = _items[i]; - if (item.NeedsUpdate) { try @@ -128,18 +138,12 @@ namespace Avalonia.Rendering } } } + _updateItemsCopy.Clear(); Interlocked.Exchange(ref _inUpdate, 0); }, DispatcherPriority.Render); } - lock (_items) - { - _itemsCopy.Clear(); - foreach (var i in _items) - _itemsCopy.Add(i); - } - for (int i = 0; i < _itemsCopy.Count; i++) { _itemsCopy[i].Render(); diff --git a/src/Avalonia.Base/Rendering/SceneGraph/BitmapBlendModeNode.cs b/src/Avalonia.Base/Rendering/SceneGraph/BitmapBlendModeNode.cs index 98e89f6549..b1190a159b 100644 --- a/src/Avalonia.Base/Rendering/SceneGraph/BitmapBlendModeNode.cs +++ b/src/Avalonia.Base/Rendering/SceneGraph/BitmapBlendModeNode.cs @@ -27,7 +27,7 @@ namespace Avalonia.Rendering.SceneGraph } /// - public Rect Bounds => Rect.Empty; + public Rect Bounds => default; /// /// Gets the BitmapBlend to be pushed or null if the operation represents a pop. diff --git a/src/Avalonia.Base/Rendering/SceneGraph/ClipNode.cs b/src/Avalonia.Base/Rendering/SceneGraph/ClipNode.cs index 90430bed02..e1bfaa4aa3 100644 --- a/src/Avalonia.Base/Rendering/SceneGraph/ClipNode.cs +++ b/src/Avalonia.Base/Rendering/SceneGraph/ClipNode.cs @@ -40,7 +40,7 @@ namespace Avalonia.Rendering.SceneGraph } /// - public Rect Bounds => Rect.Empty; + public Rect Bounds => default; /// /// Gets the clip to be pushed or null if the operation represents a pop. diff --git a/src/Avalonia.Base/Rendering/SceneGraph/GeometryClipNode.cs b/src/Avalonia.Base/Rendering/SceneGraph/GeometryClipNode.cs index 667b66420b..842edf2bcb 100644 --- a/src/Avalonia.Base/Rendering/SceneGraph/GeometryClipNode.cs +++ b/src/Avalonia.Base/Rendering/SceneGraph/GeometryClipNode.cs @@ -28,7 +28,7 @@ namespace Avalonia.Rendering.SceneGraph } /// - public Rect Bounds => Rect.Empty; + public Rect Bounds => default; /// /// Gets the clip to be pushed or null if the operation represents a pop. diff --git a/src/Avalonia.Base/Rendering/SceneGraph/OpacityMaskNode.cs b/src/Avalonia.Base/Rendering/SceneGraph/OpacityMaskNode.cs index 5fd200ddff..3ecc07fa54 100644 --- a/src/Avalonia.Base/Rendering/SceneGraph/OpacityMaskNode.cs +++ b/src/Avalonia.Base/Rendering/SceneGraph/OpacityMaskNode.cs @@ -19,7 +19,7 @@ namespace Avalonia.Rendering.SceneGraph /// The bounds of the mask. /// Auxiliary data required to draw the brush. public OpacityMaskNode(IBrush mask, Rect bounds, IDisposable? aux = null) - : base(Rect.Empty, Matrix.Identity, aux) + : base(default, Matrix.Identity, aux) { Mask = mask.ToImmutable(); MaskBounds = bounds; @@ -30,7 +30,7 @@ namespace Avalonia.Rendering.SceneGraph /// opacity mask pop. /// public OpacityMaskNode() - : base(Rect.Empty, Matrix.Identity, null) + : base(default, Matrix.Identity, null) { } diff --git a/src/Avalonia.Base/Rendering/SceneGraph/OpacityNode.cs b/src/Avalonia.Base/Rendering/SceneGraph/OpacityNode.cs index 8fc630588f..e41e639067 100644 --- a/src/Avalonia.Base/Rendering/SceneGraph/OpacityNode.cs +++ b/src/Avalonia.Base/Rendering/SceneGraph/OpacityNode.cs @@ -26,7 +26,7 @@ namespace Avalonia.Rendering.SceneGraph } /// - public Rect Bounds => Rect.Empty; + public Rect Bounds => default; /// /// Gets the opacity to be pushed or null if the operation represents a pop. diff --git a/src/Avalonia.Base/Rendering/SceneGraph/Scene.cs b/src/Avalonia.Base/Rendering/SceneGraph/Scene.cs index 2da8923497..735eb3bb3f 100644 --- a/src/Avalonia.Base/Rendering/SceneGraph/Scene.cs +++ b/src/Avalonia.Base/Rendering/SceneGraph/Scene.cs @@ -346,5 +346,7 @@ namespace Avalonia.Rendering.SceneGraph } public void MarkAsRendered() => _rendered.TrySetResult(true); + + public List? RenderThreadJobs { get; set; } } } diff --git a/src/Avalonia.Base/Rendering/SceneGraph/SceneBuilder.cs b/src/Avalonia.Base/Rendering/SceneGraph/SceneBuilder.cs index d54bd3fad8..55ff772772 100644 --- a/src/Avalonia.Base/Rendering/SceneGraph/SceneBuilder.cs +++ b/src/Avalonia.Base/Rendering/SceneGraph/SceneBuilder.cs @@ -331,8 +331,8 @@ namespace Avalonia.Rendering.SceneGraph scene.Size = newSize; - Rect horizontalDirtyRect = Rect.Empty; - Rect verticalDirtyRect = Rect.Empty; + Rect horizontalDirtyRect = default; + Rect verticalDirtyRect = default; if (newSize.Width > oldSize.Width) { @@ -429,7 +429,7 @@ namespace Avalonia.Rendering.SceneGraph else { layer.OpacityMask = null; - layer.OpacityMaskRect = Rect.Empty; + layer.OpacityMaskRect = default; } layer.GeometryClip = node.HasAncestorGeometryClip ? diff --git a/src/Avalonia.Base/Size.cs b/src/Avalonia.Base/Size.cs index 5f20206200..aec237afae 100644 --- a/src/Avalonia.Base/Size.cs +++ b/src/Avalonia.Base/Size.cs @@ -28,8 +28,9 @@ namespace Avalonia public static readonly Size Infinity = new Size(double.PositiveInfinity, double.PositiveInfinity); /// - /// A size representing zero + /// A size representing zero. /// + [Obsolete("Use the default keyword instead.")] public static readonly Size Empty = new Size(0, 0); /// @@ -309,9 +310,6 @@ namespace Avalonia /// /// Gets a value indicating whether the Width and Height values are zero. /// - public bool IsDefault - { - get { return (_width == 0) && (_height == 0); } - } + public bool IsDefault => (_width == 0) && (_height == 0); } } diff --git a/src/Avalonia.Base/Styling/SelectorMatch.cs b/src/Avalonia.Base/Styling/SelectorMatch.cs index 26b525347e..2eac04301a 100644 --- a/src/Avalonia.Base/Styling/SelectorMatch.cs +++ b/src/Avalonia.Base/Styling/SelectorMatch.cs @@ -43,7 +43,7 @@ namespace Avalonia.Styling /// A selector match describes whether and how a matches a control, and /// in addition whether the selector can ever match a control of the same type. /// - public readonly struct SelectorMatch + public readonly record struct SelectorMatch { /// /// A selector match with the result of . diff --git a/src/Avalonia.Base/Thickness.cs b/src/Avalonia.Base/Thickness.cs index c8e6d7dfd2..f9e4355edd 100644 --- a/src/Avalonia.Base/Thickness.cs +++ b/src/Avalonia.Base/Thickness.cs @@ -97,10 +97,9 @@ namespace Avalonia /// public double Bottom => _bottom; - /// - /// Gets a value indicating whether all sides are set to 0. - /// - public bool IsEmpty => Left.Equals(0) && IsUniform; + /// + [Obsolete("Use IsDefault instead.")] + public bool IsEmpty => IsDefault; /// /// Gets a value indicating whether all sides are equal. @@ -292,15 +291,13 @@ namespace Avalonia left = this._left; top = this._top; right = this._right; - bottom = this._bottom; + bottom = this._bottom; } /// - /// Gets a value indicating whether the left, top, right and bottom thickness values are zero. + /// Gets a value indicating whether the instance has default values + /// (the left, top, right and bottom values are zero). /// - public bool IsDefault - { - get { return (_left == 0) && (_top == 0) && (_right == 0) && (_bottom == 0); } - } + public bool IsDefault => (_left == 0) && (_top == 0) && (_right == 0) && (_bottom == 0); } } diff --git a/src/Avalonia.Base/Utilities/ArraySlice.cs b/src/Avalonia.Base/Utilities/ArraySlice.cs index 482f807fe0..39c0cd5556 100644 --- a/src/Avalonia.Base/Utilities/ArraySlice.cs +++ b/src/Avalonia.Base/Utilities/ArraySlice.cs @@ -111,14 +111,6 @@ namespace Avalonia.Utilities } } - /// - /// Defines an implicit conversion of a to a - /// - public static implicit operator ReadOnlySlice(ArraySlice slice) - { - return new ReadOnlySlice(slice._data, 0, slice.Length, slice.Start); - } - /// /// Defines an implicit conversion of an array to a /// diff --git a/src/Avalonia.Base/Utilities/ReadOnlySlice.cs b/src/Avalonia.Base/Utilities/ReadOnlySlice.cs deleted file mode 100644 index 583a3139b9..0000000000 --- a/src/Avalonia.Base/Utilities/ReadOnlySlice.cs +++ /dev/null @@ -1,239 +0,0 @@ -using System; -using System.Collections; -using System.Collections.Generic; -using System.Diagnostics; -using System.Runtime.CompilerServices; - -namespace Avalonia.Utilities -{ - /// - /// ReadOnlySlice enables the ability to work with a sequence within a region of memory and retains the position in within that region. - /// - /// The type of elements in the slice. - [DebuggerTypeProxy(typeof(ReadOnlySlice<>.ReadOnlySliceDebugView))] - public readonly struct ReadOnlySlice : IReadOnlyList where T : struct - { - private readonly int _bufferOffset; - - /// - /// Gets an empty - /// - public static ReadOnlySlice Empty => new ReadOnlySlice(Array.Empty()); - - private readonly ReadOnlyMemory _buffer; - - public ReadOnlySlice(ReadOnlyMemory buffer) : this(buffer, 0, buffer.Length) { } - - public ReadOnlySlice(ReadOnlyMemory buffer, int start, int length, int bufferOffset = 0) - { -#if DEBUG - if (start.CompareTo(0) < 0) - { - throw new ArgumentOutOfRangeException(nameof (start)); - } - - if (length.CompareTo(buffer.Length) > 0) - { - throw new ArgumentOutOfRangeException(nameof (length)); - } -#endif - - _buffer = buffer; - Start = start; - Length = length; - _bufferOffset = bufferOffset; - } - - /// - /// Gets the start. - /// - /// - /// The start. - /// - public int Start { get; } - - /// - /// Gets the end. - /// - /// - /// The end. - /// - public int End => Start + Length - 1; - - /// - /// Gets the length. - /// - /// - /// The length. - /// - public int Length { get; } - - /// - /// Gets a value that indicates whether this instance of is Empty. - /// - public bool IsEmpty => Length == 0; - - /// - /// Get the underlying span. - /// - public ReadOnlySpan Span => _buffer.Span.Slice(_bufferOffset, Length); - - /// - /// Get the buffer offset. - /// - public int BufferOffset => _bufferOffset; - - /// - /// Get the underlying buffer. - /// - public ReadOnlyMemory Buffer => _buffer; - - /// - /// Returns a value to specified element of the slice. - /// - /// The index of the element to return. - /// The . - /// - /// Thrown when index less than 0 or index greater than or equal to . - /// - public T this[int index] - { - [MethodImpl(MethodImplOptions.AggressiveInlining)] - get - { -#if DEBUG - if (index.CompareTo(0) < 0 || index.CompareTo(Length) > 0) - { - throw new ArgumentOutOfRangeException(nameof (index)); - } -#endif - return Span[index]; - } - } - - /// - /// Returns a sub slice of elements that start at the specified index and has the specified number of elements. - /// - /// The start of the sub slice. - /// The length of the sub slice. - /// A that contains the specified number of elements from the specified start. - public ReadOnlySlice AsSlice(int start, int length) - { - if (IsEmpty) - { - return this; - } - - if (length == 0) - { - return Empty; - } - - if (start < 0 || _bufferOffset + start > _buffer.Length - 1) - { - throw new ArgumentOutOfRangeException(nameof(start)); - } - - if (_bufferOffset + start + length > _buffer.Length) - { - throw new ArgumentOutOfRangeException(nameof(length)); - } - - return new ReadOnlySlice(_buffer, start, length, _bufferOffset); - } - - /// - /// Returns a specified number of contiguous elements from the start of the slice. - /// - /// The number of elements to return. - /// A that contains the specified number of elements from the start of this slice. - public ReadOnlySlice Take(int length) - { - if (IsEmpty) - { - return this; - } - - if (length > Length) - { - throw new ArgumentOutOfRangeException(nameof(length)); - } - - return new ReadOnlySlice(_buffer, Start, length, _bufferOffset); - } - - /// - /// Bypasses a specified number of elements in the slice and then returns the remaining elements. - /// - /// The number of elements to skip before returning the remaining elements. - /// A that contains the elements that occur after the specified index in this slice. - public ReadOnlySlice Skip(int length) - { - if (IsEmpty) - { - return this; - } - - if (length > Length) - { - throw new ArgumentOutOfRangeException(nameof(length)); - } - - return new ReadOnlySlice(_buffer, Start + length, Length - length, _bufferOffset + length); - } - - /// - /// Returns an enumerator for the slice. - /// - public ImmutableReadOnlyListStructEnumerator GetEnumerator() - { - return new ImmutableReadOnlyListStructEnumerator(this); - } - - IEnumerator IEnumerable.GetEnumerator() - { - return GetEnumerator(); - } - - IEnumerator IEnumerable.GetEnumerator() - { - return GetEnumerator(); - } - - int IReadOnlyCollection.Count => Length; - - T IReadOnlyList.this[int index] => this[index]; - - public static implicit operator ReadOnlySlice(T[] array) - { - return new ReadOnlySlice(array); - } - - public static implicit operator ReadOnlySlice(ReadOnlyMemory memory) - { - return new ReadOnlySlice(memory); - } - - public static implicit operator ReadOnlySpan(ReadOnlySlice slice) => slice.Span; - - internal class ReadOnlySliceDebugView - { - private readonly ReadOnlySlice _readOnlySlice; - - public ReadOnlySliceDebugView(ReadOnlySlice readOnlySlice) - { - _readOnlySlice = readOnlySlice; - } - - public int Start => _readOnlySlice.Start; - - public int End => _readOnlySlice.End; - - public int Length => _readOnlySlice.Length; - - public bool IsEmpty => _readOnlySlice.IsEmpty; - - public ReadOnlySpan Items => _readOnlySlice.Span; - } - } -} diff --git a/src/Avalonia.Base/Utilities/SmallDictionary.cs b/src/Avalonia.Base/Utilities/SmallDictionary.cs index 7d6a21c136..8aaf2200df 100644 --- a/src/Avalonia.Base/Utilities/SmallDictionary.cs +++ b/src/Avalonia.Base/Utilities/SmallDictionary.cs @@ -5,7 +5,7 @@ using System.Diagnostics.CodeAnalysis; namespace Avalonia.Utilities; -public struct InlineDictionary : IEnumerable> where TKey : class where TValue : class +public record struct InlineDictionary : IEnumerable> where TKey : class where TValue : class { object? _data; TValue? _value; diff --git a/src/Avalonia.Base/Utilities/StringTokenizer.cs b/src/Avalonia.Base/Utilities/StringTokenizer.cs index 748eb09209..726c9735ef 100644 --- a/src/Avalonia.Base/Utilities/StringTokenizer.cs +++ b/src/Avalonia.Base/Utilities/StringTokenizer.cs @@ -8,7 +8,7 @@ namespace Avalonia.Utilities #if !BUILDTASK public #endif - struct StringTokenizer : IDisposable + record struct StringTokenizer : IDisposable { private const char DefaultSeparatorChar = ','; diff --git a/src/Avalonia.Base/Utilities/SynchronousCompletionAsyncResult.cs b/src/Avalonia.Base/Utilities/SynchronousCompletionAsyncResult.cs index fb8d3ed224..e574462e2c 100644 --- a/src/Avalonia.Base/Utilities/SynchronousCompletionAsyncResult.cs +++ b/src/Avalonia.Base/Utilities/SynchronousCompletionAsyncResult.cs @@ -8,7 +8,7 @@ namespace Avalonia.Utilities /// A task-like operation that is guaranteed to finish continuations synchronously, /// can be used for parametrized one-shot events /// - public struct SynchronousCompletionAsyncResult : INotifyCompletion + public record struct SynchronousCompletionAsyncResult : INotifyCompletion { private readonly SynchronousCompletionAsyncResultSource? _source; private readonly T? _result; diff --git a/src/Avalonia.Base/Utilities/ValueSpan.cs b/src/Avalonia.Base/Utilities/ValueSpan.cs index 7a10d865ef..4c2de2776d 100644 --- a/src/Avalonia.Base/Utilities/ValueSpan.cs +++ b/src/Avalonia.Base/Utilities/ValueSpan.cs @@ -3,7 +3,7 @@ /// /// Pairing of value and positions sharing that value. /// - public readonly struct ValueSpan + public readonly record struct ValueSpan { public ValueSpan(int start, int length, T value) { diff --git a/src/Avalonia.Base/Visual.cs b/src/Avalonia.Base/Visual.cs index 7694119589..e3dc4fbb75 100644 --- a/src/Avalonia.Base/Visual.cs +++ b/src/Avalonia.Base/Visual.cs @@ -89,6 +89,14 @@ namespace Avalonia public static readonly StyledProperty RenderTransformOriginProperty = AvaloniaProperty.Register(nameof(RenderTransformOrigin), defaultValue: RelativePoint.Center); + /// + /// Defines the property. + /// + public static readonly AttachedProperty FlowDirectionProperty = + AvaloniaProperty.RegisterAttached( + nameof(FlowDirection), + inherits: true); + /// /// Defines the property. /// @@ -263,6 +271,15 @@ namespace Avalonia set { SetValue(RenderTransformOriginProperty, value); } } + /// + /// Gets or sets the text flow direction. + /// + public FlowDirection FlowDirection + { + get => GetValue(FlowDirectionProperty); + set => SetValue(FlowDirectionProperty, value); + } + /// /// Gets or sets the Z index of the control. /// @@ -306,6 +323,36 @@ namespace Avalonia /// internal Visual? VisualParent => _visualParent; + /// + /// Gets a value indicating whether control bypass FlowDirecton policies. + /// + /// + /// Related to FlowDirection system and returns false as default, so if + /// is RTL then control will get a mirror presentation. + /// For controls that want to avoid this behavior, override this property and return true. + /// + protected virtual bool BypassFlowDirectionPolicies => false; + + /// + /// Gets the value of the attached on a control. + /// + /// The control. + /// The flow direction. + public static FlowDirection GetFlowDirection(Visual visual) + { + return visual.GetValue(FlowDirectionProperty); + } + + /// + /// Sets the value of the attached on a control. + /// + /// The control. + /// The property value to set. + public static void SetFlowDirection(Visual visual, FlowDirection value) + { + visual.SetValue(FlowDirectionProperty, value); + } + /// /// Invalidates the visual and queues a repaint. /// @@ -387,6 +434,22 @@ namespace Avalonia } } + /// + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + { + base.OnPropertyChanged(change); + + if (change.Property == FlowDirectionProperty) + { + InvalidateMirrorTransform(); + + foreach (var child in VisualChildren) + { + child.InvalidateMirrorTransform(); + } + } + } + protected override void LogicalChildrenCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e) { base.LogicalChildrenCollectionChanged(sender, e); @@ -682,5 +745,32 @@ namespace Avalonia visual.SetVisualParent(parent); } } + + /// + /// Computes the value according to the + /// and + /// + public virtual void InvalidateMirrorTransform() + { + var flowDirection = this.FlowDirection; + var parentFlowDirection = FlowDirection.LeftToRight; + + bool bypassFlowDirectionPolicies = BypassFlowDirectionPolicies; + bool parentBypassFlowDirectionPolicies = false; + + var parent = VisualParent; + if (parent != null) + { + parentFlowDirection = parent.FlowDirection; + parentBypassFlowDirectionPolicies = parent.BypassFlowDirectionPolicies; + } + + bool thisShouldBeMirrored = flowDirection == FlowDirection.RightToLeft && !bypassFlowDirectionPolicies; + bool parentShouldBeMirrored = parentFlowDirection == FlowDirection.RightToLeft && !parentBypassFlowDirectionPolicies; + + bool shouldApplyMirrorTransform = thisShouldBeMirrored != parentShouldBeMirrored; + + HasMirrorTransform = shouldApplyMirrorTransform; + } } } diff --git a/src/Avalonia.Build.Tasks/GenerateAvaloniaResourcesTask.cs b/src/Avalonia.Build.Tasks/GenerateAvaloniaResourcesTask.cs index fad6ad397b..65681a2d28 100644 --- a/src/Avalonia.Build.Tasks/GenerateAvaloniaResourcesTask.cs +++ b/src/Avalonia.Build.Tasks/GenerateAvaloniaResourcesTask.cs @@ -7,7 +7,7 @@ using System.Text; using Avalonia.Markup.Xaml.PortableXaml; using Avalonia.Utilities; using Microsoft.Build.Framework; -using SPath=System.IO.Path; +using SPath = System.IO.Path; namespace Avalonia.Build.Tasks { public class GenerateAvaloniaResourcesTask : ITask @@ -30,12 +30,17 @@ namespace Avalonia.Build.Tasks private byte[] _data; private string _sourcePath; - public Source(string relativePath, string root) + public Source(ITaskItem avaloniaResourceItem, string root) { root = SPath.GetFullPath(root); - Path = "/" + relativePath.Replace('\\', '/'); + var relativePath = avaloniaResourceItem.ItemSpec; _sourcePath = SPath.Combine(root, relativePath); Size = (int)new FileInfo(_sourcePath).Length; + var link = avaloniaResourceItem.GetMetadata("Link"); + var path = !string.IsNullOrEmpty(link) + ? link + : relativePath; + Path = "/" + path.Replace('\\', '/'); } public string SystemPath => _sourcePath ?? Path; @@ -65,8 +70,7 @@ namespace Avalonia.Build.Tasks List BuildResourceSources() => Resources.Select(r => { - - var src = new Source(r.ItemSpec, Root); + var src = new Source(r, Root); BuildEngine.LogMessage(FormattableString.Invariant($"avares -> name:{src.Path}, path: {src.SystemPath}, size:{src.Size}, ItemSpec:{r.ItemSpec}"), _reportImportance); return src; }).ToList(); @@ -93,15 +97,15 @@ namespace Avalonia.Build.Tasks ms.CopyTo(output); foreach (var s in sources) { - using(var input = s.Open()) + using (var input = s.Open()) input.CopyTo(output); } } private bool PreProcessXamlFiles(List sources) { - var typeToXamlIndex = new Dictionary(); - + var typeToXamlIndex = new Dictionary(); + foreach (var s in sources.ToArray()) { if (s.Path.ToLowerInvariant().EndsWith(".xaml") || s.Path.ToLowerInvariant().EndsWith(".paml") || s.Path.ToLowerInvariant().EndsWith(".axaml")) @@ -111,7 +115,7 @@ namespace Avalonia.Build.Tasks { info = XamlFileInfo.Parse(s.ReadAsString()); } - catch(Exception e) + catch (Exception e) { BuildEngine.LogError(BuildEngineErrorCode.InvalidXAML, s.SystemPath, "File doesn't contain valid XAML: " + e); return false; @@ -121,7 +125,7 @@ namespace Avalonia.Build.Tasks { if (typeToXamlIndex.ContainsKey(info.XClass)) { - + BuildEngine.LogError(BuildEngineErrorCode.DuplicateXClass, s.SystemPath, $"Duplicate x:Class directive, {info.XClass} is already used in {typeToXamlIndex[info.XClass]}"); return false; diff --git a/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorPicker.xaml b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorPicker.xaml index 9d2a994e5b..2ccf20d460 100644 --- a/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorPicker.xaml +++ b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorPicker.xaml @@ -3,10 +3,6 @@ xmlns:controls="using:Avalonia.Controls" xmlns:primitives="using:Avalonia.Controls.Primitives"> - - - - diff --git a/src/Avalonia.Controls.ColorPicker/Themes/Fluent/Fluent.xaml b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/Fluent.xaml index 85f4e417e6..2cc8a1d38a 100644 --- a/src/Avalonia.Controls.ColorPicker/Themes/Fluent/Fluent.xaml +++ b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/Fluent.xaml @@ -50,13 +50,13 @@ - - - + + + - - + + diff --git a/src/Avalonia.Controls.ColorPicker/Themes/Simple/ColorPicker.xaml b/src/Avalonia.Controls.ColorPicker/Themes/Simple/ColorPicker.xaml index 626ddd4b43..b82d36a288 100644 --- a/src/Avalonia.Controls.ColorPicker/Themes/Simple/ColorPicker.xaml +++ b/src/Avalonia.Controls.ColorPicker/Themes/Simple/ColorPicker.xaml @@ -3,10 +3,6 @@ xmlns:controls="using:Avalonia.Controls" xmlns:primitives="using:Avalonia.Controls.Primitives"> - - - - diff --git a/src/Avalonia.Controls.ColorPicker/Themes/Simple/Simple.xaml b/src/Avalonia.Controls.ColorPicker/Themes/Simple/Simple.xaml index 7aefa23706..8c4dfa9c87 100644 --- a/src/Avalonia.Controls.ColorPicker/Themes/Simple/Simple.xaml +++ b/src/Avalonia.Controls.ColorPicker/Themes/Simple/Simple.xaml @@ -50,13 +50,13 @@ - - - + + + - - + + diff --git a/src/Avalonia.Controls.DataGrid/DataGrid.cs b/src/Avalonia.Controls.DataGrid/DataGrid.cs index 454678c64b..5dcad83601 100644 --- a/src/Avalonia.Controls.DataGrid/DataGrid.cs +++ b/src/Avalonia.Controls.DataGrid/DataGrid.cs @@ -48,7 +48,6 @@ namespace Avalonia.Controls private const string DATAGRID_elementColumnHeadersPresenterName = "PART_ColumnHeadersPresenter"; private const string DATAGRID_elementFrozenColumnScrollBarSpacerName = "PART_FrozenColumnScrollBarSpacer"; private const string DATAGRID_elementHorizontalScrollbarName = "PART_HorizontalScrollbar"; - private const string DATAGRID_elementRowHeadersPresenterName = "PART_RowHeadersPresenter"; private const string DATAGRID_elementTopLeftCornerHeaderName = "PART_TopLeftCornerHeader"; private const string DATAGRID_elementTopRightCornerHeaderName = "PART_TopRightCornerHeader"; private const string DATAGRID_elementBottomRightCornerHeaderName = "PART_BottomRightCorner"; @@ -1124,7 +1123,7 @@ namespace Avalonia.Controls EnsureColumnHeadersVisibility(); if (!newValueCols) { - _columnHeadersPresenter.Measure(Size.Empty); + _columnHeadersPresenter.Measure(default(Size)); } else { @@ -1165,7 +1164,7 @@ namespace Avalonia.Controls _topLeftCornerHeader.IsVisible = newValueRows && newValueCols; if (_topLeftCornerHeader.IsVisible) { - _topLeftCornerHeader.Measure(Size.Empty); + _topLeftCornerHeader.Measure(default(Size)); } } diff --git a/src/Avalonia.Controls.DataGrid/DataGridCheckBoxColumn.cs b/src/Avalonia.Controls.DataGrid/DataGridCheckBoxColumn.cs index 6b1796e50b..c308312398 100644 --- a/src/Avalonia.Controls.DataGrid/DataGridCheckBoxColumn.cs +++ b/src/Avalonia.Controls.DataGrid/DataGridCheckBoxColumn.cs @@ -192,14 +192,14 @@ namespace Avalonia.Controls void OnLayoutUpdated(object sender, EventArgs e) { - if(!editingCheckBox.Bounds.IsEmpty) + if(!editingCheckBox.Bounds.IsDefault) { editingCheckBox.LayoutUpdated -= OnLayoutUpdated; ProcessPointerArgs(); } } - if(editingCheckBox.Bounds.IsEmpty) + if(editingCheckBox.Bounds.IsDefault) { editingCheckBox.LayoutUpdated += OnLayoutUpdated; } diff --git a/src/Avalonia.Controls.DataGrid/DataGridColumnHeader.cs b/src/Avalonia.Controls.DataGrid/DataGridColumnHeader.cs index 6473c9c051..252868847a 100644 --- a/src/Avalonia.Controls.DataGrid/DataGridColumnHeader.cs +++ b/src/Avalonia.Controls.DataGrid/DataGridColumnHeader.cs @@ -34,7 +34,6 @@ namespace Avalonia.Controls } private const int DATAGRIDCOLUMNHEADER_resizeRegionWidth = 5; - private const double DATAGRIDCOLUMNHEADER_separatorThickness = 1; private const int DATAGRIDCOLUMNHEADER_columnsDragTreshold = 5; private bool _areHandlersSuspended; diff --git a/src/Avalonia.Controls.DataGrid/DataGridRowHeader.cs b/src/Avalonia.Controls.DataGrid/DataGridRowHeader.cs index fe3ba0abf6..c42d21d126 100644 --- a/src/Avalonia.Controls.DataGrid/DataGridRowHeader.cs +++ b/src/Avalonia.Controls.DataGrid/DataGridRowHeader.cs @@ -18,8 +18,6 @@ namespace Avalonia.Controls.Primitives public class DataGridRowHeader : ContentControl { private const string DATAGRIDROWHEADER_elementRootName = "PART_Root"; - private const double DATAGRIDROWHEADER_separatorThickness = 1; - private Control _rootElement; public static readonly StyledProperty SeparatorBrushProperty = diff --git a/src/Avalonia.Controls.DataGrid/DataGridTemplateColumn.cs b/src/Avalonia.Controls.DataGrid/DataGridTemplateColumn.cs index 3ec78d6d6a..24ae358dcc 100644 --- a/src/Avalonia.Controls.DataGrid/DataGridTemplateColumn.cs +++ b/src/Avalonia.Controls.DataGrid/DataGridTemplateColumn.cs @@ -64,9 +64,11 @@ namespace Avalonia.Controls protected override Control GenerateElement(DataGridCell cell, object dataItem) { - if(CellTemplate != null) + if (CellTemplate != null) { - return CellTemplate.Build(dataItem); + return (CellTemplate is IRecyclingDataTemplate recyclingDataTemplate) + ? recyclingDataTemplate.Build(dataItem, cell.Content as Control) + : CellTemplate.Build(dataItem); } if (Design.IsDesignMode) { diff --git a/src/Avalonia.Controls.DataGrid/Primitives/DataGridCellsPresenter.cs b/src/Avalonia.Controls.DataGrid/Primitives/DataGridCellsPresenter.cs index 38d559a031..06a77f0894 100644 --- a/src/Avalonia.Controls.DataGrid/Primitives/DataGridCellsPresenter.cs +++ b/src/Avalonia.Controls.DataGrid/Primitives/DataGridCellsPresenter.cs @@ -161,7 +161,7 @@ namespace Avalonia.Controls.Primitives { // Clip RectangleGeometry rg = new RectangleGeometry(); - rg.Rect = Rect.Empty; + rg.Rect = default; cell.Clip = rg; } } diff --git a/src/Avalonia.Controls.DataGrid/Primitives/DataGridColumnHeadersPresenter.cs b/src/Avalonia.Controls.DataGrid/Primitives/DataGridColumnHeadersPresenter.cs index b34f52f47d..f9b84793c6 100644 --- a/src/Avalonia.Controls.DataGrid/Primitives/DataGridColumnHeadersPresenter.cs +++ b/src/Avalonia.Controls.DataGrid/Primitives/DataGridColumnHeadersPresenter.cs @@ -305,7 +305,7 @@ namespace Avalonia.Controls.Primitives } if (!OwningGrid.AreColumnHeadersVisible) { - return Size.Empty; + return default; } double height = OwningGrid.ColumnHeaderHeight; bool autoSizeHeight; diff --git a/src/Avalonia.Controls.DataGrid/Primitives/DataGridDetailsPresenter.cs b/src/Avalonia.Controls.DataGrid/Primitives/DataGridDetailsPresenter.cs index 543485b311..07e7708003 100644 --- a/src/Avalonia.Controls.DataGrid/Primitives/DataGridDetailsPresenter.cs +++ b/src/Avalonia.Controls.DataGrid/Primitives/DataGridDetailsPresenter.cs @@ -112,7 +112,7 @@ namespace Avalonia.Controls.Primitives { if (OwningGrid == null || Children.Count == 0) { - return Size.Empty; + return default; } double desiredWidth = OwningGrid.AreRowDetailsFrozen ? diff --git a/src/Avalonia.Controls.DataGrid/Themes/Fluent.xaml b/src/Avalonia.Controls.DataGrid/Themes/Fluent.xaml index fb4335f4de..dd8575c989 100644 --- a/src/Avalonia.Controls.DataGrid/Themes/Fluent.xaml +++ b/src/Avalonia.Controls.DataGrid/Themes/Fluent.xaml @@ -6,8 +6,8 @@ M1875 1011l-787 787v-1798h-128v1798l-787 -787l-90 90l941 941l941 -941z M1965 947l-941 -941l-941 941l90 90l787 -787v1798h128v-1798l787 787z - M515 93l930 931l-930 931l90 90l1022 -1021l-1022 -1021z - M109 486 19 576 1024 1581 2029 576 1939 486 1024 1401z + M515 93l930 931l-930 931l90 90l1022 -1021l-1022 -1021z + M109 486 19 576 1024 1581 2029 576 1939 486 1024 1401z @@ -379,7 +379,7 @@ HorizontalAlignment="Center" VerticalAlignment="Center"> @@ -387,7 +387,7 @@ diff --git a/src/Avalonia.Controls/AcrylicPlatformCompensationLevels.cs b/src/Avalonia.Controls/AcrylicPlatformCompensationLevels.cs index e1f1c4029e..2402de56ee 100644 --- a/src/Avalonia.Controls/AcrylicPlatformCompensationLevels.cs +++ b/src/Avalonia.Controls/AcrylicPlatformCompensationLevels.cs @@ -5,7 +5,7 @@ /// It controls the base opacity level of the 'tracing paper' layer that compensates /// for low blur radius. /// - public struct AcrylicPlatformCompensationLevels + public record struct AcrylicPlatformCompensationLevels { public AcrylicPlatformCompensationLevels(double transparent, double blurred, double acrylic) { diff --git a/src/Avalonia.Controls/AppBuilderBase.cs b/src/Avalonia.Controls/AppBuilderBase.cs index 604f2369e7..a100d38d78 100644 --- a/src/Avalonia.Controls/AppBuilderBase.cs +++ b/src/Avalonia.Controls/AppBuilderBase.cs @@ -229,8 +229,8 @@ namespace Avalonia.Controls s_setupWasAlreadyCalled = true; _optionsInitializers?.Invoke(); RuntimePlatformServicesInitializer(); - WindowingSubsystemInitializer(); RenderingSubsystemInitializer(); + WindowingSubsystemInitializer(); AfterPlatformServicesSetupCallback(Self); Instance = _appFactory(); Instance.ApplicationLifetime = _lifetime; diff --git a/src/Avalonia.Controls/ComboBox.cs b/src/Avalonia.Controls/ComboBox.cs index 2b407cc42a..f02df2e9c1 100644 --- a/src/Avalonia.Controls/ComboBox.cs +++ b/src/Avalonia.Controls/ComboBox.cs @@ -454,10 +454,9 @@ namespace Avalonia.Controls { if (SelectionBoxItem is Rectangle rectangle) { - if ((rectangle.Fill as VisualBrush)?.Visual is Control content) + if ((rectangle.Fill as VisualBrush)?.Visual is Visual content) { - var flowDirection = (((Visual)content!).VisualParent as Control)?.FlowDirection ?? - FlowDirection.LeftToRight; + var flowDirection = content.VisualParent?.FlowDirection ?? FlowDirection.LeftToRight; rectangle.FlowDirection = flowDirection; } } diff --git a/src/Avalonia.Controls/Control.cs b/src/Avalonia.Controls/Control.cs index 063e6ae7c8..88c9823952 100644 --- a/src/Avalonia.Controls/Control.cs +++ b/src/Avalonia.Controls/Control.cs @@ -91,13 +91,6 @@ namespace Avalonia.Controls RoutedEvent.Register( nameof(SizeChanged), RoutingStrategies.Direct); - /// - /// Defines the property. - /// - public static readonly AttachedProperty FlowDirectionProperty = - AvaloniaProperty.RegisterAttached( - nameof(FlowDirection), - inherits: true); // Note the following: // _loadedQueue : @@ -170,15 +163,6 @@ namespace Avalonia.Controls get => GetValue(TagProperty); set => SetValue(TagProperty, value); } - - /// - /// Gets or sets the text flow direction. - /// - public FlowDirection FlowDirection - { - get => GetValue(FlowDirectionProperty); - set => SetValue(FlowDirectionProperty, value); - } /// /// Occurs when the user has completed a context input gesture, such as a right-click. @@ -229,39 +213,9 @@ namespace Avalonia.Controls public new Control? Parent => (Control?)base.Parent; - /// - /// Gets the value of the attached on a control. - /// - /// The control. - /// The flow direction. - public static FlowDirection GetFlowDirection(Control control) - { - return control.GetValue(FlowDirectionProperty); - } - - /// - /// Sets the value of the attached on a control. - /// - /// The control. - /// The property value to set. - public static void SetFlowDirection(Control control, FlowDirection value) - { - control.SetValue(FlowDirectionProperty, value); - } - /// bool IDataTemplateHost.IsDataTemplatesInitialized => _dataTemplates != null; - /// - /// Gets a value indicating whether control bypass FlowDirecton policies. - /// - /// - /// Related to FlowDirection system and returns false as default, so if - /// is RTL then control will get a mirror presentation. - /// For controls that want to avoid this behavior, override this property and return true. - /// - protected virtual bool BypassFlowDirectionPolicies => false; - /// void ISetterValue.Initialize(ISetter setter) { @@ -571,45 +525,6 @@ namespace Avalonia.Controls RaiseEvent(sizeChangedEventArgs); } } - else if (change.Property == FlowDirectionProperty) - { - InvalidateMirrorTransform(); - - foreach (var visual in VisualChildren) - { - if (visual is Control child) - { - child.InvalidateMirrorTransform(); - } - } - } - } - - /// - /// Computes the value according to the - /// and - /// - public virtual void InvalidateMirrorTransform() - { - var flowDirection = this.FlowDirection; - var parentFlowDirection = FlowDirection.LeftToRight; - - bool bypassFlowDirectionPolicies = BypassFlowDirectionPolicies; - bool parentBypassFlowDirectionPolicies = false; - - var parent = this.VisualParent as Control; - if (parent != null) - { - parentFlowDirection = parent.FlowDirection; - parentBypassFlowDirectionPolicies = parent.BypassFlowDirectionPolicies; - } - - bool thisShouldBeMirrored = flowDirection == FlowDirection.RightToLeft && !bypassFlowDirectionPolicies; - bool parentShouldBeMirrored = parentFlowDirection == FlowDirection.RightToLeft && !parentBypassFlowDirectionPolicies; - - bool shouldApplyMirrorTransform = thisShouldBeMirrored != parentShouldBeMirrored; - - HasMirrorTransform = shouldApplyMirrorTransform; } } } diff --git a/src/Avalonia.Controls/Documents/LineBreak.cs b/src/Avalonia.Controls/Documents/LineBreak.cs index 108a38d86b..ee31b85be9 100644 --- a/src/Avalonia.Controls/Documents/LineBreak.cs +++ b/src/Avalonia.Controls/Documents/LineBreak.cs @@ -4,7 +4,7 @@ using System.Text; using Avalonia.Media.TextFormatting; using Avalonia.Metadata; -namespace Avalonia.Controls.Documents +namespace Avalonia.Controls.Documents { /// /// LineBreak element that forces a line breaking. @@ -21,7 +21,7 @@ namespace Avalonia.Controls.Documents internal override void BuildTextRun(IList textRuns) { - var text = Environment.NewLine.AsMemory(); + var text = Environment.NewLine; var textRunProperties = CreateTextRunProperties(); diff --git a/src/Avalonia.Controls/Documents/Run.cs b/src/Avalonia.Controls/Documents/Run.cs index 2bd66b8a64..5d7b8998e6 100644 --- a/src/Avalonia.Controls/Documents/Run.cs +++ b/src/Avalonia.Controls/Documents/Run.cs @@ -52,7 +52,7 @@ namespace Avalonia.Controls.Documents internal override void BuildTextRun(IList textRuns) { - var text = (Text ?? "").AsMemory(); + var text = Text ?? ""; var textRunProperties = CreateTextRunProperties(); diff --git a/src/Avalonia.Controls/Embedding/EmbeddableControlRoot.cs b/src/Avalonia.Controls/Embedding/EmbeddableControlRoot.cs index 5ff0fd1feb..b79fef55b9 100644 --- a/src/Avalonia.Controls/Embedding/EmbeddableControlRoot.cs +++ b/src/Avalonia.Controls/Embedding/EmbeddableControlRoot.cs @@ -4,7 +4,6 @@ using Avalonia.Controls.Platform; using Avalonia.Input; using Avalonia.Platform; using Avalonia.Styling; -using JetBrains.Annotations; namespace Avalonia.Controls.Embedding { @@ -12,7 +11,6 @@ namespace Avalonia.Controls.Embedding { public EmbeddableControlRoot(ITopLevelImpl impl) : base(impl) { - } public EmbeddableControlRoot() : base(PlatformManager.CreateEmbeddableWindow()) diff --git a/src/Avalonia.Controls/Embedding/Offscreen/OffscreenTopLevelImpl.cs b/src/Avalonia.Controls/Embedding/Offscreen/OffscreenTopLevelImpl.cs index 4f80892857..4a227a0c00 100644 --- a/src/Avalonia.Controls/Embedding/Offscreen/OffscreenTopLevelImpl.cs +++ b/src/Avalonia.Controls/Embedding/Offscreen/OffscreenTopLevelImpl.cs @@ -13,6 +13,7 @@ namespace Avalonia.Controls.Embedding.Offscreen { private double _scaling = 1; private Size _clientSize; + private PlatformRenderInterfaceContextManager _renderInterface = new(null); public IInputRoot? InputRoot { get; private set; } public bool IsDisposed { get; private set; } @@ -22,7 +23,8 @@ namespace Avalonia.Controls.Embedding.Offscreen IsDisposed = true; } - public IRenderer CreateRenderer(IRenderRoot root) => new ImmediateRenderer((Visual)root); + public IRenderer CreateRenderer(IRenderRoot root) => + new ImmediateRenderer((Visual)root, () => _renderInterface.CreateRenderTarget(Surfaces), _renderInterface); public abstract void Invalidate(Rect rect); public abstract IEnumerable Surfaces { get; } diff --git a/src/Avalonia.Controls/Flyouts/FlyoutBase.cs b/src/Avalonia.Controls/Flyouts/FlyoutBase.cs index 3ff248f0be..8455495830 100644 --- a/src/Avalonia.Controls/Flyouts/FlyoutBase.cs +++ b/src/Avalonia.Controls/Flyouts/FlyoutBase.cs @@ -435,7 +435,7 @@ namespace Avalonia.Controls.Primitives { Size sz; // Popup.Child can't be null here, it was set in ShowAtCore. - if (Popup.Child!.DesiredSize == Size.Empty) + if (Popup.Child!.DesiredSize.IsDefault) { // Popup may not have been shown yet. Measure content sz = LayoutHelper.MeasureChild(Popup.Child, Size.Infinity, new Thickness()); @@ -457,7 +457,7 @@ namespace Avalonia.Controls.Primitives PopupPositioning.PopupPositionerConstraintAdjustment.SlideY; } - var trgtBnds = Target?.Bounds ?? Rect.Empty; + var trgtBnds = Target?.Bounds ?? default; switch (Placement) { diff --git a/src/Avalonia.Controls/LayoutTransformControl.cs b/src/Avalonia.Controls/LayoutTransformControl.cs index 5668b79e81..766a712c88 100644 --- a/src/Avalonia.Controls/LayoutTransformControl.cs +++ b/src/Avalonia.Controls/LayoutTransformControl.cs @@ -91,7 +91,7 @@ namespace Avalonia.Controls arrangedsize = TransformRoot.Bounds.Size; // This is the first opportunity under Silverlight to find out the Child's true DesiredSize - if (IsSizeSmaller(finalSizeTransformed, arrangedsize) && (Size.Empty == _childActualSize)) + if (IsSizeSmaller(finalSizeTransformed, arrangedsize) && _childActualSize.IsDefault) { //// Unfortunately, all the work so far is invalid because the wrong DesiredSize was used //// Make a note of the actual DesiredSize @@ -102,7 +102,7 @@ namespace Avalonia.Controls else { // Clear the "need to measure/arrange again" flag - _childActualSize = Size.Empty; + _childActualSize = default; } // Return result to perform the transformation @@ -122,7 +122,7 @@ namespace Avalonia.Controls } Size measureSize; - if (_childActualSize == Size.Empty) + if (_childActualSize.IsDefault) { // Determine the largest size after the transformation measureSize = ComputeLargestTransformedSize(availableSize); @@ -206,7 +206,7 @@ namespace Avalonia.Controls /// /// Actual DesiredSize of Child element (the value it returned from its MeasureOverride method). /// - private Size _childActualSize = Size.Empty; + private Size _childActualSize = default; /// /// RenderTransform/MatrixTransform applied to TransformRoot. @@ -281,7 +281,7 @@ namespace Avalonia.Controls private Size ComputeLargestTransformedSize(Size arrangeBounds) { // Computed largest transformed size - Size computedSize = Size.Empty; + Size computedSize = default; // Detect infinite bounds and constrain the scenario bool infiniteWidth = double.IsInfinity(arrangeBounds.Width); diff --git a/src/Avalonia.Controls/NativeControlHost.cs b/src/Avalonia.Controls/NativeControlHost.cs index bcf0866129..18dc1b1264 100644 --- a/src/Avalonia.Controls/NativeControlHost.cs +++ b/src/Avalonia.Controls/NativeControlHost.cs @@ -145,7 +145,7 @@ namespace Avalonia.Controls if (IsEffectivelyVisible && bounds.HasValue) { - if (bounds.Value.IsEmpty) + if (bounds.Value.IsDefault) return false; _attachment?.ShowInBounds(bounds.Value); } diff --git a/src/Avalonia.Controls/Presenters/ItemsPresenter.cs b/src/Avalonia.Controls/Presenters/ItemsPresenter.cs index 924c4567f6..96c9b7b5d9 100644 --- a/src/Avalonia.Controls/Presenters/ItemsPresenter.cs +++ b/src/Avalonia.Controls/Presenters/ItemsPresenter.cs @@ -77,7 +77,7 @@ namespace Avalonia.Controls.Presenters } /// - Size IScrollable.Extent => Virtualizer?.Extent ?? Size.Empty; + Size IScrollable.Extent => Virtualizer?.Extent ?? default; /// Vector IScrollable.Offset @@ -136,12 +136,12 @@ namespace Avalonia.Controls.Presenters /// protected override Size MeasureOverride(Size availableSize) { - return Virtualizer?.MeasureOverride(availableSize) ?? Size.Empty; + return Virtualizer?.MeasureOverride(availableSize) ?? default; } protected override Size ArrangeOverride(Size finalSize) { - return Virtualizer?.ArrangeOverride(finalSize) ?? Size.Empty; + return Virtualizer?.ArrangeOverride(finalSize) ?? default; } /// diff --git a/src/Avalonia.Controls/Presenters/TextPresenter.cs b/src/Avalonia.Controls/Presenters/TextPresenter.cs index 7eaff49910..480a8ee7a7 100644 --- a/src/Avalonia.Controls/Presenters/TextPresenter.cs +++ b/src/Avalonia.Controls/Presenters/TextPresenter.cs @@ -106,7 +106,6 @@ namespace Avalonia.Controls.Presenters private Rect _caretBounds; private Point _navigationPosition; private string? _preeditText; - private CharacterHit _compositionStartHit = new CharacterHit(-1); static TextPresenter() { diff --git a/src/Avalonia.Controls/Primitives/PopupPositioning/IPopupPositioner.cs b/src/Avalonia.Controls/Primitives/PopupPositioning/IPopupPositioner.cs index 17dfec118f..2e70947457 100644 --- a/src/Avalonia.Controls/Primitives/PopupPositioning/IPopupPositioner.cs +++ b/src/Avalonia.Controls/Primitives/PopupPositioning/IPopupPositioner.cs @@ -64,7 +64,7 @@ namespace Avalonia.Controls.Primitives.PopupPositioning /// surface. /// [Unstable] - public struct PopupPositionerParameters + public record struct PopupPositionerParameters { private PopupGravity _gravity; private PopupAnchor _anchor; diff --git a/src/Avalonia.Controls/Primitives/PopupPositioning/ManagedPopupPositioner.cs b/src/Avalonia.Controls/Primitives/PopupPositioning/ManagedPopupPositioner.cs index a80a60350e..62e5f37273 100644 --- a/src/Avalonia.Controls/Primitives/PopupPositioning/ManagedPopupPositioner.cs +++ b/src/Avalonia.Controls/Primitives/PopupPositioning/ManagedPopupPositioner.cs @@ -112,7 +112,7 @@ namespace Avalonia.Controls.Primitives.PopupPositioning ?? screens.FirstOrDefault(s => s.Bounds.Intersects(parentGeometry)) ?? screens.FirstOrDefault(); - if (targetScreen != null && targetScreen.WorkingArea.IsEmpty) + if (targetScreen != null && targetScreen.WorkingArea.IsDefault) { return targetScreen.Bounds; } diff --git a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs index 44fa78ac21..795bc05e23 100644 --- a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs +++ b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs @@ -119,8 +119,6 @@ namespace Avalonia.Controls.Primitives /// public static readonly StyledProperty WrapSelectionProperty = AvaloniaProperty.Register(nameof(WrapSelection), defaultValue: false); - - private static readonly IList Empty = Array.Empty(); private string _textSearchTerm = string.Empty; private DispatcherTimer? _textSearchTimer; private ISelectionModel? _selection; diff --git a/src/Avalonia.Controls/PullToRefresh/RefreshCompletionDeferral.cs b/src/Avalonia.Controls/PullToRefresh/RefreshCompletionDeferral.cs new file mode 100644 index 0000000000..a18b3c2934 --- /dev/null +++ b/src/Avalonia.Controls/PullToRefresh/RefreshCompletionDeferral.cs @@ -0,0 +1,36 @@ +using System; +using System.Threading; + +namespace Avalonia.Controls +{ + /// + /// Deferral class for notify that a work done in RefreshRequested event is done. + /// + public class RefreshCompletionDeferral + { + private Action _deferredAction; + private int _deferCount; + + public RefreshCompletionDeferral(Action deferredAction) + { + _deferredAction = deferredAction; + } + + public void Complete() + { + Interlocked.Decrement(ref _deferCount); + + if (_deferCount == 0) + { + _deferredAction?.Invoke(); + } + } + + public RefreshCompletionDeferral Get() + { + Interlocked.Increment(ref _deferCount); + + return this; + } + } +} diff --git a/src/Avalonia.Controls/PullToRefresh/RefreshContainer.cs b/src/Avalonia.Controls/PullToRefresh/RefreshContainer.cs new file mode 100644 index 0000000000..d0b8178add --- /dev/null +++ b/src/Avalonia.Controls/PullToRefresh/RefreshContainer.cs @@ -0,0 +1,252 @@ +using System; +using Avalonia.Controls.Primitives; +using Avalonia.Controls.PullToRefresh; +using Avalonia.Input; +using Avalonia.Interactivity; + +namespace Avalonia.Controls +{ + /// + /// Represents a container control that provides a and pull-to-refresh functionality for scrollable content. + /// + public class RefreshContainer : ContentControl + { + internal const int DefaultPullDimensionSize = 100; + + private bool _hasDefaultRefreshInfoProviderAdapter; + + private ScrollViewerIRefreshInfoProviderAdapter? _refreshInfoProviderAdapter; + private RefreshInfoProvider? _refreshInfoProvider; + private IDisposable? _visualizerSizeSubscription; + private Grid? _visualizerPresenter; + private RefreshVisualizer? _refreshVisualizer; + private bool _hasDefaultRefreshVisualizer; + + /// + /// Defines the event. + /// + public static readonly RoutedEvent RefreshRequestedEvent = + RoutedEvent.Register(nameof(RefreshRequested), RoutingStrategies.Bubble); + + internal static readonly DirectProperty RefreshInfoProviderAdapterProperty = + AvaloniaProperty.RegisterDirect(nameof(RefreshInfoProviderAdapter), + (s) => s.RefreshInfoProviderAdapter, (s, o) => s.RefreshInfoProviderAdapter = o); + + /// + /// Defines the event. + /// + public static readonly DirectProperty VisualizerProperty = + AvaloniaProperty.RegisterDirect(nameof(Visualizer), + s => s.Visualizer, (s, o) => s.Visualizer = o); + + /// + /// Defines the event. + /// + public static readonly StyledProperty PullDirectionProperty = + AvaloniaProperty.Register(nameof(PullDirection), PullDirection.TopToBottom); + + internal ScrollViewerIRefreshInfoProviderAdapter? RefreshInfoProviderAdapter + { + get => _refreshInfoProviderAdapter; set + { + _hasDefaultRefreshInfoProviderAdapter = false; + SetAndRaise(RefreshInfoProviderAdapterProperty, ref _refreshInfoProviderAdapter, value); + } + } + + /// + /// Gets or sets the for this container. + /// + public RefreshVisualizer? Visualizer + { + get => _refreshVisualizer; set + { + if (_refreshVisualizer != null) + { + _visualizerSizeSubscription?.Dispose(); + _refreshVisualizer.RefreshRequested -= Visualizer_RefreshRequested; + } + + SetAndRaise(VisualizerProperty, ref _refreshVisualizer, value); + } + } + + /// + /// Gets or sets a value that specifies the direction to pull to initiate a refresh. + /// + public PullDirection PullDirection + { + get => GetValue(PullDirectionProperty); + set => SetValue(PullDirectionProperty, value); + } + + /// + /// Occurs when an update of the content has been initiated. + /// + public event EventHandler? RefreshRequested + { + add => AddHandler(RefreshRequestedEvent, value); + remove => RemoveHandler(RefreshRequestedEvent, value); + } + + public RefreshContainer() + { + _hasDefaultRefreshInfoProviderAdapter = true; + _refreshInfoProviderAdapter = new ScrollViewerIRefreshInfoProviderAdapter(PullDirection); + RaisePropertyChanged(RefreshInfoProviderAdapterProperty, null, _refreshInfoProviderAdapter); + } + + protected override void OnApplyTemplate(TemplateAppliedEventArgs e) + { + base.OnApplyTemplate(e); + + _visualizerPresenter = e.NameScope.Find("PART_RefreshVisualizerPresenter"); + + if (_refreshVisualizer == null) + { + _hasDefaultRefreshVisualizer = true; + Visualizer = new RefreshVisualizer(); + } + else + { + _hasDefaultRefreshVisualizer = false; + RaisePropertyChanged(VisualizerProperty, default, _refreshVisualizer); + } + + OnPullDirectionChanged(); + } + + private void OnVisualizerSizeChanged(Rect obj) + { + if (_hasDefaultRefreshInfoProviderAdapter) + { + _refreshInfoProviderAdapter = new ScrollViewerIRefreshInfoProviderAdapter(PullDirection); + RaisePropertyChanged(RefreshInfoProviderAdapterProperty, null, _refreshInfoProviderAdapter); + } + } + + private void Visualizer_RefreshRequested(object? sender, RefreshRequestedEventArgs e) + { + var ev = new RefreshRequestedEventArgs(e.GetDeferral(), RefreshRequestedEvent); + RaiseEvent(ev); + ev.DecrementCount(); + } + + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + { + base.OnPropertyChanged(change); + + if (change.Property == RefreshInfoProviderAdapterProperty) + { + if (_refreshVisualizer != null) + { + if (_refreshInfoProvider != null) + { + _refreshVisualizer.RefreshInfoProvider = _refreshInfoProvider; + } + else + { + if (RefreshInfoProviderAdapter != null && _refreshVisualizer != null) + { + _refreshInfoProvider = RefreshInfoProviderAdapter?.AdaptFromTree(this, _refreshVisualizer.Bounds.Size); + + if (_refreshInfoProvider != null) + { + _refreshVisualizer.RefreshInfoProvider = _refreshInfoProvider; + RefreshInfoProviderAdapter?.SetAnimations(_refreshVisualizer); + } + } + } + } + } + else if (change.Property == VisualizerProperty) + { + if (_visualizerPresenter != null) + { + _visualizerPresenter.Children.Clear(); + if (_refreshVisualizer != null) + { + _visualizerPresenter.Children.Add(_refreshVisualizer); + } + } + + if (_refreshVisualizer != null) + { + _refreshVisualizer.RefreshRequested += Visualizer_RefreshRequested; + _visualizerSizeSubscription = _refreshVisualizer.GetObservable(Control.BoundsProperty).Subscribe(OnVisualizerSizeChanged); + } + } + else if (change.Property == PullDirectionProperty) + { + OnPullDirectionChanged(); + } + } + + private void OnPullDirectionChanged() + { + if (_visualizerPresenter != null && _refreshVisualizer != null) + { + switch (PullDirection) + { + case PullDirection.TopToBottom: + _visualizerPresenter.VerticalAlignment = Layout.VerticalAlignment.Top; + _visualizerPresenter.HorizontalAlignment = Layout.HorizontalAlignment.Stretch; + if (_hasDefaultRefreshVisualizer) + { + _refreshVisualizer.PullDirection = PullDirection.TopToBottom; + _refreshVisualizer.Height = DefaultPullDimensionSize; + _refreshVisualizer.Width = double.NaN; + } + break; + case PullDirection.BottomToTop: + _visualizerPresenter.VerticalAlignment = Layout.VerticalAlignment.Bottom; + _visualizerPresenter.HorizontalAlignment = Layout.HorizontalAlignment.Stretch; + if (_hasDefaultRefreshVisualizer) + { + _refreshVisualizer.PullDirection = PullDirection.BottomToTop; + _refreshVisualizer.Height = DefaultPullDimensionSize; + _refreshVisualizer.Width = double.NaN; + } + break; + case PullDirection.LeftToRight: + _visualizerPresenter.VerticalAlignment = Layout.VerticalAlignment.Stretch; + _visualizerPresenter.HorizontalAlignment = Layout.HorizontalAlignment.Left; + if (_hasDefaultRefreshVisualizer) + { + _refreshVisualizer.PullDirection = PullDirection.LeftToRight; + _refreshVisualizer.Width = DefaultPullDimensionSize; + _refreshVisualizer.Height = double.NaN; + } + break; + case PullDirection.RightToLeft: + _visualizerPresenter.VerticalAlignment = Layout.VerticalAlignment.Stretch; + _visualizerPresenter.HorizontalAlignment = Layout.HorizontalAlignment.Right; + if (_hasDefaultRefreshVisualizer) + { + _refreshVisualizer.PullDirection = PullDirection.RightToLeft; + _refreshVisualizer.Width = DefaultPullDimensionSize; + _refreshVisualizer.Height = double.NaN; + } + break; + } + + if (_hasDefaultRefreshInfoProviderAdapter && + _hasDefaultRefreshVisualizer && + _refreshVisualizer.Bounds.Height == DefaultPullDimensionSize && + _refreshVisualizer.Bounds.Width == DefaultPullDimensionSize) + { + _refreshInfoProviderAdapter = new ScrollViewerIRefreshInfoProviderAdapter(PullDirection); + RaisePropertyChanged(RefreshInfoProviderAdapterProperty, null, _refreshInfoProviderAdapter); + } + } + } + + /// + /// Initiates an update of the content. + /// + public void RequestRefresh() + { + _refreshVisualizer?.RequestRefresh(); + } + } +} diff --git a/src/Avalonia.Controls/PullToRefresh/RefreshInfoProvider.cs b/src/Avalonia.Controls/PullToRefresh/RefreshInfoProvider.cs new file mode 100644 index 0000000000..847f011fa9 --- /dev/null +++ b/src/Avalonia.Controls/PullToRefresh/RefreshInfoProvider.cs @@ -0,0 +1,141 @@ +using System; +using Avalonia.Input; +using Avalonia.Interactivity; +using Avalonia.Rendering.Composition; + +namespace Avalonia.Controls.PullToRefresh +{ + internal class RefreshInfoProvider : Interactive + { + internal const double DefaultExecutionRatio = 0.8; + + private readonly PullDirection _refreshPullDirection; + private readonly Size _refreshVisualizerSize; + + private readonly CompositionVisual? _visual; + private bool _isInteractingForRefresh; + private double _interactionRatio; + private bool _entered; + + public DirectProperty IsInteractingForRefreshProperty = + AvaloniaProperty.RegisterDirect(nameof(IsInteractingForRefresh), + s => s.IsInteractingForRefresh, (s, o) => s.IsInteractingForRefresh = o); + + + public DirectProperty ExecutionRatioProperty = + AvaloniaProperty.RegisterDirect(nameof(ExecutionRatio), + s => s.ExecutionRatio); + + public DirectProperty InteractionRatioProperty = + AvaloniaProperty.RegisterDirect(nameof(InteractionRatio), + s => s.InteractionRatio, (s, o) => s.InteractionRatio = o); + + /// + /// Defines the event. + /// + public static readonly RoutedEvent RefreshStartedEvent = + RoutedEvent.Register(nameof(RefreshStarted), RoutingStrategies.Bubble); + + /// + /// Defines the event. + /// + public static readonly RoutedEvent RefreshCompletedEvent = + RoutedEvent.Register(nameof(RefreshCompleted), RoutingStrategies.Bubble); + + public bool PeekingMode { get; internal set; } + + public bool IsInteractingForRefresh + { + get => _isInteractingForRefresh; + internal set + { + var isInteractingForRefresh = value && !PeekingMode; + + if (isInteractingForRefresh != _isInteractingForRefresh) + { + SetAndRaise(IsInteractingForRefreshProperty, ref _isInteractingForRefresh, isInteractingForRefresh); + } + } + } + + public double InteractionRatio + { + get => _interactionRatio; + set + { + SetAndRaise(InteractionRatioProperty, ref _interactionRatio, value); + } + } + + public double ExecutionRatio + { + get => DefaultExecutionRatio; + } + + internal CompositionVisual? Visual => _visual; + + public event EventHandler RefreshStarted + { + add => AddHandler(RefreshStartedEvent, value); + remove => RemoveHandler(RefreshStartedEvent, value); + } + + public event EventHandler RefreshCompleted + { + add => AddHandler(RefreshCompletedEvent, value); + remove => RemoveHandler(RefreshCompletedEvent, value); + } + + internal void InteractingStateEntered(object? sender, PullGestureEventArgs e) + { + if (!_entered) + { + IsInteractingForRefresh = true; + _entered = true; + } + + ValuesChanged(e.Delta); + } + + internal void InteractingStateExited(object? sender, PullGestureEndedEventArgs e) + { + IsInteractingForRefresh = false; + _entered = false; + + ValuesChanged(default); + } + + + public RefreshInfoProvider(PullDirection refreshPullDirection, Size? refreshVIsualizerSize, CompositionVisual? visual) + { + _refreshPullDirection = refreshPullDirection; + _refreshVisualizerSize = refreshVIsualizerSize ?? default; + _visual = visual; + } + + public void OnRefreshStarted() + { + RaiseEvent(new RoutedEventArgs(RefreshStartedEvent)); + } + + public void OnRefreshCompleted() + { + RaiseEvent(new RoutedEventArgs(RefreshCompletedEvent)); + } + + internal void ValuesChanged(Vector value) + { + switch (_refreshPullDirection) + { + case PullDirection.TopToBottom: + case PullDirection.BottomToTop: + InteractionRatio = _refreshVisualizerSize.Height == 0 ? 1 : Math.Min(1, value.Y / _refreshVisualizerSize.Height); + break; + case PullDirection.LeftToRight: + case PullDirection.RightToLeft: + InteractionRatio = _refreshVisualizerSize.Height == 0 ? 1 : Math.Min(1, value.X / _refreshVisualizerSize.Width); + break; + } + } + } +} diff --git a/src/Avalonia.Controls/PullToRefresh/RefreshRequestedEventArgs.cs b/src/Avalonia.Controls/PullToRefresh/RefreshRequestedEventArgs.cs new file mode 100644 index 0000000000..4bb25d3b2c --- /dev/null +++ b/src/Avalonia.Controls/PullToRefresh/RefreshRequestedEventArgs.cs @@ -0,0 +1,42 @@ +using System; +using Avalonia.Interactivity; + +namespace Avalonia.Controls +{ + /// + /// Provides event data for RefreshRequested events. + /// + public class RefreshRequestedEventArgs : RoutedEventArgs + { + private RefreshCompletionDeferral _refreshCompletionDeferral; + + /// + /// Gets a deferral object for managing the work done in the RefreshRequested event handler. + /// + /// A object + public RefreshCompletionDeferral GetDeferral() + { + return _refreshCompletionDeferral.Get(); + } + + public RefreshRequestedEventArgs(Action deferredAction, RoutedEvent? routedEvent) : base(routedEvent) + { + _refreshCompletionDeferral = new RefreshCompletionDeferral(deferredAction); + } + + public RefreshRequestedEventArgs(RefreshCompletionDeferral completionDeferral, RoutedEvent? routedEvent) : base(routedEvent) + { + _refreshCompletionDeferral = completionDeferral; + } + + internal void IncrementCount() + { + _refreshCompletionDeferral?.Get(); + } + + internal void DecrementCount() + { + _refreshCompletionDeferral?.Complete(); + } + } +} diff --git a/src/Avalonia.Controls/PullToRefresh/RefreshVisualizer.cs b/src/Avalonia.Controls/PullToRefresh/RefreshVisualizer.cs new file mode 100644 index 0000000000..f2f735aaa9 --- /dev/null +++ b/src/Avalonia.Controls/PullToRefresh/RefreshVisualizer.cs @@ -0,0 +1,553 @@ +using System; +using System.Numerics; +using System.Reactive.Linq; +using Avalonia.Animation.Easings; +using Avalonia.Controls.Primitives; +using Avalonia.Controls.PullToRefresh; +using Avalonia.Input; +using Avalonia.Interactivity; +using Avalonia.Media; +using Avalonia.Rendering.Composition; +using Avalonia.Rendering.Composition.Animations; + +namespace Avalonia.Controls +{ + public class RefreshVisualizer : ContentControl + { + private const int DefaultIndicatorSize = 24; + private const float MinimumIndicatorOpacity = 0.4f; + private const float ParallaxPositionRatio = 0.5f; + private double _executingRatio = 0.8; + + private RefreshVisualizerState _refreshVisualizerState; + private RefreshInfoProvider? _refreshInfoProvider; + private IDisposable? _isInteractingSubscription; + private IDisposable? _interactionRatioSubscription; + private bool _isInteractingForRefresh; + private Grid? _root; + private Control? _content; + private RefreshVisualizerOrientation _orientation; + private float _startingRotationAngle; + private double _interactionRatio; + private bool _played; + private ScalarKeyFrameAnimation? _rotateAnimation; + + private bool IsPullDirectionVertical => PullDirection == PullDirection.TopToBottom || PullDirection == PullDirection.BottomToTop; + private bool IsPullDirectionFar => PullDirection == PullDirection.BottomToTop || PullDirection == PullDirection.RightToLeft; + + /// + /// Defines the property. + /// + public static readonly StyledProperty PullDirectionProperty = + AvaloniaProperty.Register(nameof(PullDirection), PullDirection.TopToBottom); + + /// + /// Defines the event. + /// + public static readonly RoutedEvent RefreshRequestedEvent = + RoutedEvent.Register(nameof(RefreshRequested), RoutingStrategies.Bubble); + + /// + /// Defines the property. + /// + public static readonly DirectProperty RefreshVisualizerStateProperty = + AvaloniaProperty.RegisterDirect(nameof(RefreshVisualizerState), + s => s.RefreshVisualizerState); + + /// + /// Defines the property. + /// + public static readonly DirectProperty OrientationProperty = + AvaloniaProperty.RegisterDirect(nameof(Orientation), + s => s.Orientation, (s, o) => s.Orientation = o); + + /// + /// Defines the property. + /// + internal DirectProperty RefreshInfoProviderProperty = + AvaloniaProperty.RegisterDirect(nameof(RefreshInfoProvider), + s => s.RefreshInfoProvider, (s, o) => s.RefreshInfoProvider = o); + + /// + /// Gets or sets a value that indicates the refresh state of the visualizer. + /// + protected RefreshVisualizerState RefreshVisualizerState + { + get + { + return _refreshVisualizerState; + } + private set + { + SetAndRaise(RefreshVisualizerStateProperty, ref _refreshVisualizerState, value); + UpdateContent(); + } + } + + /// + /// Gets or sets a value that indicates the orientation of the visualizer. + /// + public RefreshVisualizerOrientation Orientation + { + get + { + return _orientation; + } + set + { + SetAndRaise(OrientationProperty, ref _orientation, value); + } + } + + internal PullDirection PullDirection + { + get => GetValue(PullDirectionProperty); + set => SetValue(PullDirectionProperty, value); + } + + internal RefreshInfoProvider? RefreshInfoProvider + { + get => _refreshInfoProvider; + set + { + if (_refreshInfoProvider != null) + { + _refreshInfoProvider.RenderTransform = null; + } + SetAndRaise(RefreshInfoProviderProperty, ref _refreshInfoProvider, value); + } + } + + /// + /// Occurs when an update of the content has been initiated. + /// + public event EventHandler? RefreshRequested + { + add => AddHandler(RefreshRequestedEvent, value); + remove => RemoveHandler(RefreshRequestedEvent, value); + } + + protected override void OnApplyTemplate(TemplateAppliedEventArgs e) + { + base.OnApplyTemplate(e); + + this.ClipToBounds = false; + + _root = e.NameScope.Find("PART_Root"); + + if (_root != null) + { + _content = Content as Control; + + if (_content == null) + { + _content = new PathIcon() + { + Height = DefaultIndicatorSize, + Width = DefaultIndicatorSize, + Name = "PART_Icon" + }; + + _content.Loaded += (s, e) => + { + var composition = ElementComposition.GetElementVisual(_content); + var compositor = composition!.Compositor; + composition.Opacity = 0; + + var smoothRotationAnimation + = compositor.CreateScalarKeyFrameAnimation(); + smoothRotationAnimation.Target = "RotationAngle"; + smoothRotationAnimation.InsertExpressionKeyFrame(1.0f, "this.FinalValue", new LinearEasing()); + smoothRotationAnimation.Duration = TimeSpan.FromMilliseconds(100); + + var opacityAnimation + = compositor.CreateScalarKeyFrameAnimation(); + opacityAnimation.Target = "Opacity"; + opacityAnimation.InsertExpressionKeyFrame(1.0f, "this.FinalValue", new LinearEasing()); + opacityAnimation.Duration = TimeSpan.FromMilliseconds(100); + + var offsetAnimation = compositor.CreateVector3KeyFrameAnimation(); + offsetAnimation.Target = "Offset"; + offsetAnimation.InsertExpressionKeyFrame(1.0f, "this.FinalValue", new LinearEasing()); + offsetAnimation.Duration = TimeSpan.FromMilliseconds(150); + + var scaleAnimation + = compositor.CreateVector3KeyFrameAnimation(); + scaleAnimation.Target = "Scale"; + scaleAnimation.InsertExpressionKeyFrame(1.0f, "this.FinalValue", new LinearEasing()); + scaleAnimation.Duration = TimeSpan.FromMilliseconds(100); + + var animation = compositor.CreateImplicitAnimationCollection(); + animation["RotationAngle"] = smoothRotationAnimation; + animation["Offset"] = offsetAnimation; + animation["Scale"] = scaleAnimation; + animation["Opacity"] = opacityAnimation; + + composition.ImplicitAnimations = animation; + + UpdateContent(); + }; + + Content = _content; + } + else + { + RaisePropertyChanged(ContentProperty, null, Content, Data.BindingPriority.Style, false); + } + } + + OnOrientationChanged(); + + UpdateContent(); + } + + protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) + { + base.OnAttachedToVisualTree(e); + + UpdateContent(); + } + + private void UpdateContent() + { + if (_content != null && _root != null) + { + var root = _root; + var visual = _refreshInfoProvider?.Visual; + var contentVisual = ElementComposition.GetElementVisual(_content); + var visualizerVisual = ElementComposition.GetElementVisual(this); + if (visual != null && contentVisual != null && visualizerVisual != null) + { + contentVisual.CenterPoint = new Vector3((float)(_content.Bounds.Width / 2), (float)(_content.Bounds.Height / 2), 0); + switch (RefreshVisualizerState) + { + case RefreshVisualizerState.Idle: + _played = false; + if(_rotateAnimation != null) + { + _rotateAnimation.IterationBehavior = AnimationIterationBehavior.Count; + _rotateAnimation = null; + } + contentVisual.Opacity = MinimumIndicatorOpacity; + contentVisual.RotationAngle = _startingRotationAngle; + visualizerVisual.Offset = IsPullDirectionVertical ? + new Vector3(visualizerVisual.Offset.X, 0, 0) : + new Vector3(0, visualizerVisual.Offset.Y, 0); + visual.Offset = default; + _content.InvalidateMeasure(); + break; + case RefreshVisualizerState.Interacting: + _played = false; + contentVisual.Opacity = MinimumIndicatorOpacity; + contentVisual.RotationAngle = (float)(_startingRotationAngle + _interactionRatio * 2 * Math.PI); + Vector3 offset = default; + if (IsPullDirectionVertical) + { + offset = new Vector3(0, (float)(_interactionRatio * (IsPullDirectionFar ? -1 : 1) * root.Bounds.Height), 0); + } + else + { + offset = new Vector3((float)(_interactionRatio * (IsPullDirectionFar ? -1 : 1) * root.Bounds.Width), 0, 0); + } + visual.Offset = offset; + visualizerVisual.Offset = IsPullDirectionVertical ? + new Vector3(visualizerVisual.Offset.X, offset.Y, 0) : + new Vector3(offset.X, visualizerVisual.Offset.Y, 0); + break; + case RefreshVisualizerState.Pending: + contentVisual.Opacity = 1; + contentVisual.RotationAngle = _startingRotationAngle + (float)(2 * Math.PI); + if (IsPullDirectionVertical) + { + offset = new Vector3(0, (float)(_interactionRatio * (IsPullDirectionFar ? -1 : 1) * root.Bounds.Height), 0); + } + else + { + offset = new Vector3((float)(_interactionRatio * (IsPullDirectionFar ? -1 : 1) * root.Bounds.Width), 0, 0); + } + visual.Offset = offset; + visualizerVisual.Offset = IsPullDirectionVertical ? + new Vector3(visualizerVisual.Offset.X, offset.Y, 0) : + new Vector3(offset.X, visualizerVisual.Offset.Y, 0); + + if (!_played) + { + _played = true; + var scaleAnimation = contentVisual.Compositor!.CreateVector3KeyFrameAnimation(); + scaleAnimation.Target = "Scale"; + scaleAnimation.InsertKeyFrame(0.5f, new Vector3(1.5f, 1.5f, 1)); + scaleAnimation.InsertKeyFrame(1f, new Vector3(1f, 1f, 1)); + scaleAnimation.Duration = TimeSpan.FromSeconds(0.3); + + contentVisual.StartAnimation("Scale", scaleAnimation); + } + break; + case RefreshVisualizerState.Refreshing: + _rotateAnimation = contentVisual.Compositor!.CreateScalarKeyFrameAnimation(); + _rotateAnimation.Target = "RotationAngle"; + _rotateAnimation.InsertKeyFrame(0, _startingRotationAngle, new LinearEasing()); + _rotateAnimation.InsertKeyFrame(1, _startingRotationAngle + (float)(2 * Math.PI), new LinearEasing()); + _rotateAnimation.IterationBehavior = AnimationIterationBehavior.Forever; + _rotateAnimation.StopBehavior = AnimationStopBehavior.LeaveCurrentValue; + _rotateAnimation.Duration = TimeSpan.FromSeconds(0.5); + + contentVisual.StartAnimation("RotationAngle", _rotateAnimation); + contentVisual.Opacity = 1; + float translationRatio = (float)(_refreshInfoProvider != null ? (1.0f - _refreshInfoProvider.ExecutionRatio) * ParallaxPositionRatio : 1.0f) + * (IsPullDirectionFar ? -1f : 1f); + if (IsPullDirectionVertical) + { + offset = new Vector3(0, (float)(_executingRatio * (IsPullDirectionFar ? -1 : 1) * root.Bounds.Height), 0); + } + else + { + offset = new Vector3((float)(_executingRatio * (IsPullDirectionFar ? -1 : 1) * root.Bounds.Width), 0, 0); + } + visual.Offset = offset; + contentVisual.Offset += IsPullDirectionVertical ? new Vector3(0, (float)(translationRatio * root.Bounds.Height), 0) : + new Vector3((float)(translationRatio * root.Bounds.Width), 0, 0); + visualizerVisual.Offset = IsPullDirectionVertical ? + new Vector3(visualizerVisual.Offset.X, offset.Y, 0) : + new Vector3(offset.X, visualizerVisual.Offset.Y, 0); + break; + case RefreshVisualizerState.Peeking: + contentVisual.Opacity = 1; + contentVisual.RotationAngle = _startingRotationAngle; + break; + } + } + } + } + + /// + /// Initiates an update of the content. + /// + public void RequestRefresh() + { + RefreshVisualizerState = RefreshVisualizerState.Refreshing; + RefreshInfoProvider?.OnRefreshStarted(); + + RaiseRefreshRequested(); + } + + private void RefreshCompleted() + { + RefreshVisualizerState = RefreshVisualizerState.Idle; + + RefreshInfoProvider?.OnRefreshCompleted(); + } + + private void RaiseRefreshRequested() + { + var refreshArgs = new RefreshRequestedEventArgs(RefreshCompleted, RefreshRequestedEvent); + + refreshArgs.IncrementCount(); + + RaiseEvent(refreshArgs); + + refreshArgs.DecrementCount(); + } + + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + { + base.OnPropertyChanged(change); + + if (change.Property == RefreshInfoProviderProperty) + { + OnRefreshInfoProviderChanged(); + } + else if (change.Property == ContentProperty) + { + if (_root != null && _content != null) + { + _root.Children.Insert(0, _content); + _content.VerticalAlignment = Layout.VerticalAlignment.Center; + _content.HorizontalAlignment = Layout.HorizontalAlignment.Center; + } + + UpdateContent(); + } + else if (change.Property == OrientationProperty) + { + OnOrientationChanged(); + + UpdateContent(); + } + else if (change.Property == BoundsProperty) + { + switch (PullDirection) + { + case PullDirection.TopToBottom: + RenderTransform = new TranslateTransform(0, -Bounds.Height); + break; + case PullDirection.BottomToTop: + RenderTransform = new TranslateTransform(0, Bounds.Height); + break; + case PullDirection.LeftToRight: + RenderTransform = new TranslateTransform(-Bounds.Width, 0); + break; + case PullDirection.RightToLeft: + RenderTransform = new TranslateTransform(Bounds.Width, 0); + break; + } + + UpdateContent(); + } + else if(change.Property == PullDirectionProperty) + { + OnOrientationChanged(); + + UpdateContent(); + } + } + + private void OnOrientationChanged() + { + switch (_orientation) + { + case RefreshVisualizerOrientation.Auto: + switch (PullDirection) + { + case PullDirection.TopToBottom: + case PullDirection.BottomToTop: + _startingRotationAngle = 0.0f; + break; + case PullDirection.LeftToRight: + _startingRotationAngle = (float)(-Math.PI / 2); + break; + case PullDirection.RightToLeft: + _startingRotationAngle = (float)(Math.PI / 2); + break; + } + break; + case RefreshVisualizerOrientation.Normal: + _startingRotationAngle = 0.0f; + break; + case RefreshVisualizerOrientation.Rotate90DegreesCounterclockwise: + _startingRotationAngle = (float)(Math.PI / 2); + break; + case RefreshVisualizerOrientation.Rotate270DegreesCounterclockwise: + _startingRotationAngle = (float)(-Math.PI / 2); + break; + } + } + + private void OnRefreshInfoProviderChanged() + { + _isInteractingSubscription?.Dispose(); + _isInteractingSubscription = null; + _interactionRatioSubscription?.Dispose(); + _interactionRatioSubscription = null; + + if (RefreshInfoProvider != null) + { + _isInteractingSubscription = RefreshInfoProvider.GetObservable(RefreshInfoProvider.IsInteractingForRefreshProperty) + .Subscribe(InteractingForRefreshObserver); + + _interactionRatioSubscription = RefreshInfoProvider.GetObservable(RefreshInfoProvider.InteractionRatioProperty) + .Subscribe(InteractionRatioObserver); + + var visual = RefreshInfoProvider.Visual; + + _executingRatio = RefreshInfoProvider.ExecutionRatio; + } + else + { + _executingRatio = 1; + } + } + + private void InteractionRatioObserver(double obj) + { + var wasAtZero = _interactionRatio == 0.0; + _interactionRatio = obj; + + if (_isInteractingForRefresh) + { + if (RefreshVisualizerState == RefreshVisualizerState.Idle) + { + if (wasAtZero) + { + if (_interactionRatio > _executingRatio) + { + RefreshVisualizerState = RefreshVisualizerState.Pending; + } + else if (_interactionRatio > 0) + { + RefreshVisualizerState = RefreshVisualizerState.Interacting; + } + } + else if (_interactionRatio > 0) + { + RefreshVisualizerState = RefreshVisualizerState.Peeking; + } + } + else if (RefreshVisualizerState == RefreshVisualizerState.Interacting) + { + if (_interactionRatio <= 0) + { + RefreshVisualizerState = RefreshVisualizerState.Idle; + } + else if (_interactionRatio > _executingRatio) + { + RefreshVisualizerState = RefreshVisualizerState.Pending; + } + else + { + UpdateContent(); + } + } + else if (RefreshVisualizerState == RefreshVisualizerState.Pending) + { + if (_interactionRatio <= _executingRatio) + { + RefreshVisualizerState = RefreshVisualizerState.Interacting; + } + else if (_interactionRatio <= 0) + { + RefreshVisualizerState = RefreshVisualizerState.Idle; + } + else + { + UpdateContent(); + } + } + } + else + { + if (RefreshVisualizerState != RefreshVisualizerState.Refreshing) + { + if (_interactionRatio > 0) + { + RefreshVisualizerState = RefreshVisualizerState.Peeking; + } + else + { + RefreshVisualizerState = RefreshVisualizerState.Idle; + } + } + } + } + + private void InteractingForRefreshObserver(bool obj) + { + _isInteractingForRefresh = obj; + + if (!_isInteractingForRefresh) + { + switch (_refreshVisualizerState) + { + case RefreshVisualizerState.Pending: + RequestRefresh(); + break; + case RefreshVisualizerState.Refreshing: + // We don't want to interrupt a currently executing refresh. + break; + default: + RefreshVisualizerState = RefreshVisualizerState.Idle; + break; + } + } + } + } +} diff --git a/src/Avalonia.Controls/PullToRefresh/RefreshVisualizerOrientation.cs b/src/Avalonia.Controls/PullToRefresh/RefreshVisualizerOrientation.cs new file mode 100644 index 0000000000..1ea37f67b9 --- /dev/null +++ b/src/Avalonia.Controls/PullToRefresh/RefreshVisualizerOrientation.cs @@ -0,0 +1,13 @@ +namespace Avalonia.Controls +{ + /// + /// Defines constants that specify the orientation of a RefreshVisualizer. + /// + public enum RefreshVisualizerOrientation + { + Auto, + Normal, + Rotate90DegreesCounterclockwise, + Rotate270DegreesCounterclockwise + } +} diff --git a/src/Avalonia.Controls/PullToRefresh/RefreshVisualizerState.cs b/src/Avalonia.Controls/PullToRefresh/RefreshVisualizerState.cs new file mode 100644 index 0000000000..5ab52f4de6 --- /dev/null +++ b/src/Avalonia.Controls/PullToRefresh/RefreshVisualizerState.cs @@ -0,0 +1,14 @@ +namespace Avalonia.Controls +{ + /// + /// Defines constants that specify the state of a RefreshVisualizer + /// + public enum RefreshVisualizerState + { + Idle, + Peeking, + Interacting, + Pending, + Refreshing + } +} diff --git a/src/Avalonia.Controls/PullToRefresh/ScrollViewerIRefreshInfoProviderAdapter.cs b/src/Avalonia.Controls/PullToRefresh/ScrollViewerIRefreshInfoProviderAdapter.cs new file mode 100644 index 0000000000..c3aebc82c5 --- /dev/null +++ b/src/Avalonia.Controls/PullToRefresh/ScrollViewerIRefreshInfoProviderAdapter.cs @@ -0,0 +1,274 @@ +using System; +using Avalonia.Input; +using Avalonia.Interactivity; +using Avalonia.Rendering.Composition; +using Avalonia.VisualTree; + +namespace Avalonia.Controls.PullToRefresh +{ + internal class ScrollViewerIRefreshInfoProviderAdapter + { + private const int MaxSearchDepth = 10; + private const int InitialOffsetThreshold = 1; + + private PullDirection _refreshPullDirection; + private ScrollViewer? _scrollViewer; + private RefreshInfoProvider? _refreshInfoProvider; + private PullGestureRecognizer? _pullGestureRecognizer; + private InputElement? _interactionSource; + private bool _isVisualizerInteractionSourceAttached; + + public ScrollViewerIRefreshInfoProviderAdapter(PullDirection pullDirection) + { + _refreshPullDirection = pullDirection; + } + + public RefreshInfoProvider? AdaptFromTree(Visual root, Size? refreshVIsualizerSize) + { + if (root is ScrollViewer scrollViewer) + { + return Adapt(scrollViewer, refreshVIsualizerSize); + } + else + { + int depth = 0; + while (depth < MaxSearchDepth) + { + var scroll = AdaptFromTreeRecursiveHelper(root, depth); + + if (scroll != null) + { + return Adapt(scroll, refreshVIsualizerSize); + } + + depth++; + } + } + + ScrollViewer? AdaptFromTreeRecursiveHelper(Visual root, int depth) + { + if (depth == 0) + { + foreach (var child in root.VisualChildren) + { + if (child is ScrollViewer viewer) + { + return viewer; + } + } + } + else + { + foreach (var child in root.VisualChildren) + { + var viewer = AdaptFromTreeRecursiveHelper(child, depth - 1); + if (viewer != null) + { + return viewer; + } + } + } + + return null; + } + + return null; + } + + public RefreshInfoProvider Adapt(ScrollViewer adaptee, Size? refreshVIsualizerSize) + { + if (adaptee == null) + { + throw new ArgumentNullException(nameof(adaptee), "Adaptee cannot be null"); + } + + if (_scrollViewer != null) + { + CleanUpScrollViewer(); + } + + if (_refreshInfoProvider != null && _interactionSource != null) + { + _interactionSource.RemoveHandler(Gestures.PullGestureEvent, _refreshInfoProvider.InteractingStateEntered); + _interactionSource.RemoveHandler(Gestures.PullGestureEndedEvent, _refreshInfoProvider.InteractingStateExited); + } + + _refreshInfoProvider = null; + _scrollViewer = adaptee; + + if (_scrollViewer.Content == null) + { + throw new ArgumentException(nameof(adaptee), "Adaptee's content property cannot be null."); + } + + var content = adaptee.Content as Visual; + + if (content == null) + { + throw new ArgumentException(nameof(adaptee), "Adaptee's content property must be a Visual"); + } + + if (content.GetVisualParent() == null) + { + _scrollViewer.Loaded += ScrollViewer_Loaded; + } + else + { + ScrollViewer_Loaded(null, null); + + if (content.Parent is not InputElement) + { + throw new ArgumentException(nameof(adaptee), "Adaptee's content's parent must be a InputElement"); + } + } + + _refreshInfoProvider = new RefreshInfoProvider(_refreshPullDirection, refreshVIsualizerSize, ElementComposition.GetElementVisual(content)); + + _pullGestureRecognizer = new PullGestureRecognizer(_refreshPullDirection); + + if (_interactionSource != null) + { + _interactionSource.GestureRecognizers.Add(_pullGestureRecognizer); + _interactionSource.AddHandler(Gestures.PullGestureEvent, _refreshInfoProvider.InteractingStateEntered); + _interactionSource.AddHandler(Gestures.PullGestureEndedEvent, _refreshInfoProvider.InteractingStateExited); + _isVisualizerInteractionSourceAttached = true; + } + + _scrollViewer.PointerPressed += ScrollViewer_PointerPressed; + _scrollViewer.PointerReleased += ScrollViewer_PointerReleased; + _scrollViewer.ScrollChanged += ScrollViewer_ScrollChanged; + + return _refreshInfoProvider; + } + + private void ScrollViewer_ScrollChanged(object? sender, ScrollChangedEventArgs e) + { + if (_isVisualizerInteractionSourceAttached && _refreshInfoProvider != null && _refreshInfoProvider.IsInteractingForRefresh) + { + if (!IsWithinOffsetThreashold()) + { + _refreshInfoProvider.IsInteractingForRefresh = false; + } + } + } + + public void SetAnimations(RefreshVisualizer refreshVisualizer) + { + var visualizerComposition = ElementComposition.GetElementVisual(refreshVisualizer); + if (visualizerComposition != null) + { + var compositor = visualizerComposition.Compositor; + + var offsetAnimation = compositor.CreateVector3KeyFrameAnimation(); + offsetAnimation.Target = "Offset"; + offsetAnimation.InsertExpressionKeyFrame(1.0f, "this.FinalValue"); + offsetAnimation.Duration = TimeSpan.FromMilliseconds(150); + + var animation = compositor.CreateImplicitAnimationCollection(); + animation["Offset"] = offsetAnimation; + visualizerComposition.ImplicitAnimations = animation; + } + + if(_scrollViewer != null && _scrollViewer.Content is Visual visual) + { + var scollContentComposition = ElementComposition.GetElementVisual(visual); + + if(scollContentComposition != null) + { + var compositor = scollContentComposition.Compositor; + + var offsetAnimation = compositor.CreateVector3KeyFrameAnimation(); + offsetAnimation.Target = "Offset"; + offsetAnimation.InsertExpressionKeyFrame(1.0f, "this.FinalValue"); + offsetAnimation.Duration = TimeSpan.FromMilliseconds(150); + + var animation = compositor.CreateImplicitAnimationCollection(); + animation["Offset"] = offsetAnimation; + scollContentComposition.ImplicitAnimations = animation; + } + } + } + + private void ScrollViewer_Loaded(object? sender, RoutedEventArgs? e) + { + var content = _scrollViewer?.Content as Visual; + if (content == null) + { + throw new ArgumentException(nameof(_scrollViewer), "Adaptee's content property must be a Visual"); + } + + if (content.Parent is not InputElement) + { + throw new ArgumentException(nameof(_scrollViewer), "Adaptee's content parent must be an InputElement"); + } + + MakeInteractionSource(content.Parent as InputElement); + + if (_scrollViewer != null) + { + _scrollViewer.Loaded -= ScrollViewer_Loaded; + } + } + + private void MakeInteractionSource(InputElement? element) + { + _interactionSource = element; + + if (_pullGestureRecognizer != null && _refreshInfoProvider != null) + { + element?.GestureRecognizers.Add(_pullGestureRecognizer); + _interactionSource?.AddHandler(Gestures.PullGestureEvent, _refreshInfoProvider.InteractingStateEntered); + _interactionSource?.AddHandler(Gestures.PullGestureEndedEvent, _refreshInfoProvider.InteractingStateExited); + _isVisualizerInteractionSourceAttached = true; + } + } + + private void ScrollViewer_PointerReleased(object? sender, PointerReleasedEventArgs e) + { + if (_refreshInfoProvider != null) + { + _refreshInfoProvider.IsInteractingForRefresh = false; + } + } + + private void ScrollViewer_PointerPressed(object? sender, PointerPressedEventArgs e) + { + if (_refreshInfoProvider != null) + { + _refreshInfoProvider.PeekingMode = !IsWithinOffsetThreashold(); + } + } + + private bool IsWithinOffsetThreashold() + { + if (_scrollViewer != null) + { + var offset = _scrollViewer.Offset; + + switch (_refreshPullDirection) + { + case PullDirection.TopToBottom: + return offset.Y < InitialOffsetThreshold; + case PullDirection.LeftToRight: + return offset.X < InitialOffsetThreshold; + case PullDirection.RightToLeft: + return offset.X > _scrollViewer.Extent.Width - _scrollViewer.Viewport.Width - InitialOffsetThreshold; + case PullDirection.BottomToTop: + return offset.Y > _scrollViewer.Extent.Height - _scrollViewer.Viewport.Height - InitialOffsetThreshold; + } + } + + return false; + } + + private void CleanUpScrollViewer() + { + if (_scrollViewer != null) + { + _scrollViewer.PointerPressed -= ScrollViewer_PointerPressed; + _scrollViewer.PointerReleased -= ScrollViewer_PointerReleased; + _scrollViewer.ScrollChanged -= ScrollViewer_ScrollChanged; + } + } + } +} diff --git a/src/Avalonia.Controls/Repeater/ViewportManager.cs b/src/Avalonia.Controls/Repeater/ViewportManager.cs index e24ed37f1e..10e3dd57a0 100644 --- a/src/Avalonia.Controls/Repeater/ViewportManager.cs +++ b/src/Avalonia.Controls/Repeater/ViewportManager.cs @@ -465,7 +465,7 @@ namespace Avalonia.Controls _pendingViewportShift = default; _unshiftableShift = default; - if (_visibleWindow.IsEmpty) + if (_visibleWindow.IsDefault) { // We got cleared. _layoutExtent = default; @@ -551,7 +551,7 @@ namespace Avalonia.Controls private void TryInvalidateMeasure() { // Don't invalidate measure if we have an invalid window. - if (!_visibleWindow.IsEmpty) + if (!_visibleWindow.IsDefault) { // We invalidate measure instead of just invalidating arrange because // we don't invalidate measure in UpdateViewport if the view is changing to diff --git a/src/Avalonia.Controls/Selection/ISelectionModel.cs b/src/Avalonia.Controls/Selection/ISelectionModel.cs index 3f4ae48263..4c2a355bb5 100644 --- a/src/Avalonia.Controls/Selection/ISelectionModel.cs +++ b/src/Avalonia.Controls/Selection/ISelectionModel.cs @@ -39,7 +39,7 @@ namespace Avalonia.Controls.Selection return new BatchUpdateOperation(model); } - public struct BatchUpdateOperation : IDisposable + public record struct BatchUpdateOperation : IDisposable { private readonly ISelectionModel _owner; private bool _isDisposed; diff --git a/src/Avalonia.Controls/Selection/SelectedItems.cs b/src/Avalonia.Controls/Selection/SelectedItems.cs index 4fbcfde438..ef642b7bdc 100644 --- a/src/Avalonia.Controls/Selection/SelectedItems.cs +++ b/src/Avalonia.Controls/Selection/SelectedItems.cs @@ -35,9 +35,9 @@ namespace Avalonia.Controls.Selection { return _owner.SelectedItem; } - else if (Items is object) + else if (Items is not null && Ranges is not null) { - return Items[index]; + return Items[IndexRange.GetAt(Ranges, index)]; } else { diff --git a/src/Avalonia.Controls/Selection/SelectionModel.cs b/src/Avalonia.Controls/Selection/SelectionModel.cs index 154f1868f3..9bbddfcbb2 100644 --- a/src/Avalonia.Controls/Selection/SelectionModel.cs +++ b/src/Avalonia.Controls/Selection/SelectionModel.cs @@ -738,7 +738,7 @@ namespace Avalonia.Controls.Selection } } - public struct BatchUpdateOperation : IDisposable + public record struct BatchUpdateOperation : IDisposable { private readonly SelectionModel _owner; private bool _isDisposed; diff --git a/src/Avalonia.Controls/Shapes/Shape.cs b/src/Avalonia.Controls/Shapes/Shape.cs index 1a7218ff2a..e2a13512a5 100644 --- a/src/Avalonia.Controls/Shapes/Shape.cs +++ b/src/Avalonia.Controls/Shapes/Shape.cs @@ -292,7 +292,7 @@ namespace Avalonia.Controls.Shapes return finalSize; } - return Size.Empty; + return default; } internal static (Size size, Matrix transform) CalculateSizeAndTransform(Size availableSize, Rect shapeBounds, Stretch Stretch) diff --git a/src/Avalonia.Controls/TextBlock.cs b/src/Avalonia.Controls/TextBlock.cs index c8e05e5cb3..7a0da1ecc5 100644 --- a/src/Avalonia.Controls/TextBlock.cs +++ b/src/Avalonia.Controls/TextBlock.cs @@ -630,7 +630,7 @@ namespace Avalonia.Controls } else { - textSource = new SimpleTextSource((text ?? "").AsMemory(), defaultProperties); + textSource = new SimpleTextSource(text ?? "", defaultProperties); } return new TextLayout( @@ -827,14 +827,14 @@ namespace Avalonia.Controls InvalidateTextLayout(); } - protected readonly struct SimpleTextSource : ITextSource + protected readonly record struct SimpleTextSource : ITextSource { - private readonly ReadOnlySlice _text; + private readonly CharacterBufferRange _text; private readonly TextRunProperties _defaultProperties; - public SimpleTextSource(ReadOnlySlice text, TextRunProperties defaultProperties) + public SimpleTextSource(string text, TextRunProperties defaultProperties) { - _text = text; + _text = new CharacterBufferRange(new CharacterBufferReference(text), text.Length); _defaultProperties = defaultProperties; } @@ -852,7 +852,7 @@ namespace Avalonia.Controls return new TextEndOfParagraph(); } - return new TextCharacters(runText, _defaultProperties); + return new TextCharacters(runText.CharacterBufferReference, runText.Length, _defaultProperties); } } @@ -873,21 +873,28 @@ namespace Avalonia.Controls foreach (var textRun in _textRuns) { - if (textRun.TextSourceLength == 0) + if (textRun.Length == 0) { continue; } - if (textSourceIndex >= currentPosition + textRun.TextSourceLength) + if (textSourceIndex >= currentPosition + textRun.Length) { - currentPosition += textRun.TextSourceLength; + currentPosition += textRun.Length; continue; } - if (textRun is TextCharacters) + if (textRun is TextCharacters) { - return new TextCharacters(textRun.Text.Skip(Math.Max(0, textSourceIndex - currentPosition)), textRun.Properties!); + var characterBufferReference = textRun.CharacterBufferReference; + + var skip = Math.Max(0, textSourceIndex - currentPosition); + + return new TextCharacters( + new CharacterBufferReference(characterBufferReference.CharacterBuffer, characterBufferReference.OffsetToFirstChar + skip), + textRun.Length - skip, + textRun.Properties!); } return textRun; diff --git a/src/Avalonia.Controls/TextBox.cs b/src/Avalonia.Controls/TextBox.cs index 699170df5a..1bdec878d9 100644 --- a/src/Avalonia.Controls/TextBox.cs +++ b/src/Avalonia.Controls/TextBox.cs @@ -961,7 +961,9 @@ namespace Avalonia.Controls var length = 0; - var graphemeEnumerator = new GraphemeEnumerator(input.AsMemory()); + var inputRange = new CharacterBufferRange(new CharacterBufferReference(input), input.Length); + + var graphemeEnumerator = new GraphemeEnumerator(inputRange); while (graphemeEnumerator.MoveNext()) { diff --git a/src/Avalonia.Controls/TextBoxTextInputMethodClient.cs b/src/Avalonia.Controls/TextBoxTextInputMethodClient.cs index 6a79760011..52815b943d 100644 --- a/src/Avalonia.Controls/TextBoxTextInputMethodClient.cs +++ b/src/Avalonia.Controls/TextBoxTextInputMethodClient.cs @@ -77,12 +77,14 @@ namespace Avalonia.Controls foreach (var run in textLine.TextRuns) { - if(run.Text.Length > 0) + if(run.Length > 0) { + var characterBufferRange = new CharacterBufferRange(run.CharacterBufferReference, run.Length); + #if NET6_0 - builder.Append(run.Text.Span); + builder.Append(characterBufferRange.Span); #else - builder.Append(run.Text.Span.ToArray()); + builder.Append(characterBufferRange.Span.ToArray()); #endif } } diff --git a/src/Avalonia.Controls/TopLevel.cs b/src/Avalonia.Controls/TopLevel.cs index 59ad696148..2cd55dc3ab 100644 --- a/src/Avalonia.Controls/TopLevel.cs +++ b/src/Avalonia.Controls/TopLevel.cs @@ -323,17 +323,7 @@ namespace Avalonia.Controls ??= AvaloniaLocator.Current.GetService()?.CreateProvider(this) ?? (PlatformImpl as ITopLevelImplWithStorageProvider)?.StorageProvider ?? throw new InvalidOperationException("StorageProvider platform implementation is not available."); - - IRenderTarget IRenderRoot.CreateRenderTarget() => CreateRenderTarget(); - - /// - protected virtual IRenderTarget CreateRenderTarget() - { - if(PlatformImpl == null) - throw new InvalidOperationException("Can't create render target, PlatformImpl is null (might be already disposed)"); - return _renderInterface!.CreateRenderTarget(PlatformImpl.Surfaces); - } - + /// void IRenderRoot.Invalidate(Rect rect) { @@ -357,12 +347,6 @@ namespace Avalonia.Controls /// protected virtual ILayoutManager CreateLayoutManager() => new LayoutManager(this); - public override void InvalidateMirrorTransform() - { - } - - protected override bool BypassFlowDirectionPolicies => true; - /// /// Handles a paint notification from . /// diff --git a/src/Avalonia.Controls/TreeView.cs b/src/Avalonia.Controls/TreeView.cs index 2fa4a02fa2..ffdd32f95c 100644 --- a/src/Avalonia.Controls/TreeView.cs +++ b/src/Avalonia.Controls/TreeView.cs @@ -179,6 +179,26 @@ namespace Avalonia.Controls } } + /// + /// Collapse the specified all descendent s. + /// + /// The item to collapse. + public void CollapseSubTree(TreeViewItem item) + { + item.IsExpanded = false; + + if (item.Presenter?.Panel != null) + { + foreach (var child in item.Presenter.Panel.Children) + { + if (child is TreeViewItem treeViewItem) + { + CollapseSubTree(treeViewItem); + } + } + } + } + /// /// Selects all items in the . /// diff --git a/src/Avalonia.Controls/TreeViewItem.cs b/src/Avalonia.Controls/TreeViewItem.cs index 9bfcf5adfa..18245bd682 100644 --- a/src/Avalonia.Controls/TreeViewItem.cs +++ b/src/Avalonia.Controls/TreeViewItem.cs @@ -1,3 +1,5 @@ +using System; +using System.Collections.Generic; using System.Linq; using Avalonia.Controls.Generators; using Avalonia.Controls.Metadata; @@ -166,30 +168,94 @@ namespace Avalonia.Controls { if (!e.Handled) { - switch (e.Key) + Func? handler = + e.Key switch + { + Key.Left => ApplyToItemOrRecursivelyIfCtrl(FocusAwareCollapseItem, e.KeyModifiers), + Key.Right => ApplyToItemOrRecursivelyIfCtrl(ExpandItem, e.KeyModifiers), + Key.Enter or Key.Space => ApplyToItemOrRecursivelyIfCtrl(IsExpanded ? CollapseItem : ExpandItem, e.KeyModifiers), + + // do not handle CTRL with numpad keys + Key.Subtract => FocusAwareCollapseItem, + Key.Add => ExpandItem, + Key.Divide => ApplyToSubtree(CollapseItem), + Key.Multiply => ApplyToSubtree(ExpandItem), + _ => null, + }; + + if (handler is not null) + { + e.Handled = handler(this); + } + + // NOTE: these local functions do not use the TreeView.Expand/CollapseSubtree + // function because we want to know if any items were in fact expanded to set the + // event handled status. Also the handling here avoids a potential infinite recursion/stack overflow. + static Func ApplyToSubtree(Func f) + { + // Calling toList enumerates all items before applying functions. This avoids a + // potential infinite loop if there is an infinite tree (the control catalog is + // lazily infinite). But also means a lazily loaded tree will not be expanded completely. + return t => SubTree(t) + .ToList() + .Select(treeViewItem => f(treeViewItem)) + .Aggregate(false, (p, c) => p || c); + } + + static Func ApplyToItemOrRecursivelyIfCtrl(Func f, KeyModifiers keyModifiers) + { + if (keyModifiers.HasAllFlags(KeyModifiers.Control)) + { + return ApplyToSubtree(f); + } + + return f; + } + + static bool ExpandItem(TreeViewItem treeViewItem) + { + if (treeViewItem.ItemCount > 0 && !treeViewItem.IsExpanded) + { + treeViewItem.IsExpanded = true; + return true; + } + + return false; + } + + static bool CollapseItem(TreeViewItem treeViewItem) { - case Key.Right: - if (Items != null && Items.Cast().Any() && !IsExpanded) + if (treeViewItem.ItemCount > 0 && treeViewItem.IsExpanded) + { + treeViewItem.IsExpanded = false; + return true; + } + + return false; + } + + static bool FocusAwareCollapseItem(TreeViewItem treeViewItem) + { + if (treeViewItem.ItemCount > 0 && treeViewItem.IsExpanded) + { + if (treeViewItem.IsFocused) { - IsExpanded = true; - e.Handled = true; + treeViewItem.IsExpanded = false; } - break; - - case Key.Left: - if (Items is not null && Items.Cast().Any() && IsExpanded) + else { - if (IsFocused) - { - IsExpanded = false; - } - else - { - FocusManager.Instance?.Focus(this, NavigationMethod.Directional); - } - e.Handled = true; + FocusManager.Instance?.Focus(treeViewItem, NavigationMethod.Directional); } - break; + + return true; + } + + return false; + } + + static IEnumerable SubTree(TreeViewItem treeViewItem) + { + return new[] { treeViewItem }.Concat(treeViewItem.LogicalChildren.OfType().SelectMany(child => SubTree(child))); } } @@ -198,8 +264,19 @@ namespace Avalonia.Controls protected override void OnApplyTemplate(TemplateAppliedEventArgs e) { + if (_header is InputElement previousInputMethod) + { + previousInputMethod.DoubleTapped -= HeaderDoubleTapped; + } + _header = e.NameScope.Find("PART_Header"); _templateApplied = true; + + if (_header is InputElement im) + { + im.DoubleTapped += HeaderDoubleTapped; + } + if (_deferredBringIntoViewFlag) { _deferredBringIntoViewFlag = false; @@ -220,6 +297,15 @@ namespace Avalonia.Controls return logical != null ? result : @default; } + private void HeaderDoubleTapped(object? sender, TappedEventArgs e) + { + if (ItemCount > 0) + { + IsExpanded = !IsExpanded; + e.Handled = true; + } + } + private void OnParentChanged(AvaloniaPropertyChangedEventArgs e) { if (!((ILogical)this).IsAttachedToLogicalTree && e.NewValue is null) diff --git a/src/Avalonia.Controls/Window.cs b/src/Avalonia.Controls/Window.cs index 559d674c02..b0911403cc 100644 --- a/src/Avalonia.Controls/Window.cs +++ b/src/Avalonia.Controls/Window.cs @@ -169,13 +169,11 @@ namespace Avalonia.Controls /// public static readonly RoutedEvent WindowOpenedEvent = RoutedEvent.Register("WindowOpened", RoutingStrategies.Direct); - - - - private readonly NameScope _nameScope = new NameScope(); private object? _dialogResult; private readonly Size _maxPlatformClientSize; private WindowStartupLocation _windowStartupLocation; + private bool _shown; + private bool _showingAsDialog; /// /// Initializes static members of the class. @@ -233,7 +231,7 @@ namespace Avalonia.Controls impl.GotInputWhenDisabled = OnGotInputWhenDisabled; impl.WindowStateChanged = HandleWindowStateChanged; _maxPlatformClientSize = PlatformImpl?.MaxAutoSizeHint ?? default(Size); - impl.ExtendClientAreaToDecorationsChanged = ExtendClientAreaToDecorationsChanged; + impl.ExtendClientAreaToDecorationsChanged = ExtendClientAreaToDecorationsChanged; this.GetObservable(ClientSizeProperty).Skip(1).Subscribe(x => PlatformImpl?.Resize(x, PlatformResizeReason.Application)); PlatformImpl?.ShowTaskbarIcon(ShowInTaskbar); @@ -508,6 +506,8 @@ namespace Avalonia.Controls Owner = null; PlatformImpl?.Dispose(); + + _showingAsDialog = false; } private bool ShouldCancelClose(CancelEventArgs? args = null) @@ -563,29 +563,33 @@ namespace Avalonia.Controls /// public override void Hide() { - if (!IsVisible) + using (FreezeVisibilityChangeHandling()) { - return; - } + if (!_shown) + { + return; + } - Renderer?.Stop(); + Renderer?.Stop(); - if (Owner is Window owner) - { - owner.RemoveChild(this); - } + if (Owner is Window owner) + { + owner.RemoveChild(this); + } - if (_children.Count > 0) - { - foreach (var child in _children.ToArray()) + if (_children.Count > 0) { - child.child.Hide(); + foreach (var child in _children.ToArray()) + { + child.child.Hide(); + } } - } - Owner = null; - PlatformImpl?.Hide(); - IsVisible = false; + Owner = null; + PlatformImpl?.Hide(); + IsVisible = false; + _shown = false; + } } /// @@ -599,81 +603,124 @@ namespace Avalonia.Controls ShowCore(null); } + protected override void IsVisibleChanged(AvaloniaPropertyChangedEventArgs e) + { + if (!IgnoreVisibilityChanges) + { + var isVisible = e.GetNewValue(); + + if (_shown != isVisible) + { + if(!_shown) + { + Show(); + } + else + { + if (_showingAsDialog) + { + Close(false); + } + else + { + Hide(); + } + } + } + } + } + /// - /// Shows the window as a child of . + /// Shows the window as a child of . /// - /// Window that will be a parent of the shown window. + /// Window that will be the owner of the shown window. /// /// The window has already been closed. /// - public void Show(Window parent) + public void Show(Window owner) { - if (parent is null) + if (owner is null) { - throw new ArgumentNullException(nameof(parent), "Showing a child window requires valid parent."); + throw new ArgumentNullException(nameof(owner), "Showing a child window requires valid parent."); } - ShowCore(parent); + ShowCore(owner); } - private void ShowCore(Window? parent) + private void EnsureStateBeforeShow() { if (PlatformImpl == null) { throw new InvalidOperationException("Cannot re-show a closed window."); } + } + + private void EnsureParentStateBeforeShow(Window owner) + { + if (owner.PlatformImpl == null) + { + throw new InvalidOperationException("Cannot show a window with a closed owner."); + } - if (parent != null) + if (owner == this) { - if (parent.PlatformImpl == null) - { - throw new InvalidOperationException("Cannot show a window with a closed parent."); - } - else if (parent == this) - { - throw new InvalidOperationException("A Window cannot be its own parent."); - } - else if (!parent.IsVisible) - { - throw new InvalidOperationException("Cannot show window with non-visible parent."); - } + throw new InvalidOperationException("A Window cannot be its own owner."); } - if (IsVisible) + if (!owner.IsVisible) { - return; + throw new InvalidOperationException("Cannot show window with non-visible owner."); } + } - RaiseEvent(new RoutedEventArgs(WindowOpenedEvent)); + private void ShowCore(Window? owner) + { + using (FreezeVisibilityChangeHandling()) + { + EnsureStateBeforeShow(); - EnsureInitialized(); - ApplyStyling(); - IsVisible = true; + if (owner != null) + { + EnsureParentStateBeforeShow(owner); + } - var initialSize = new Size( - double.IsNaN(Width) ? Math.Max(MinWidth, ClientSize.Width) : Width, - double.IsNaN(Height) ? Math.Max(MinHeight, ClientSize.Height) : Height); + if (_shown) + { + return; + } - if (initialSize != ClientSize) - { - PlatformImpl?.Resize(initialSize, PlatformResizeReason.Layout); - } + RaiseEvent(new RoutedEventArgs(WindowOpenedEvent)); - LayoutManager.ExecuteInitialLayoutPass(); + EnsureInitialized(); + ApplyStyling(); + _shown = true; + IsVisible = true; - if (PlatformImpl != null && parent?.PlatformImpl is not null) - { - PlatformImpl.SetParent(parent.PlatformImpl); - } + var initialSize = new Size( + double.IsNaN(Width) ? Math.Max(MinWidth, ClientSize.Width) : Width, + double.IsNaN(Height) ? Math.Max(MinHeight, ClientSize.Height) : Height); + + if (initialSize != ClientSize) + { + PlatformImpl?.Resize(initialSize, PlatformResizeReason.Layout); + } + + LayoutManager.ExecuteInitialLayoutPass(); + + if (PlatformImpl != null && owner?.PlatformImpl is not null) + { + PlatformImpl.SetParent(owner.PlatformImpl); + } - Owner = parent; - parent?.AddChild(this, false); + Owner = owner; + owner?.AddChild(this, false); - SetWindowStartupLocation(parent?.PlatformImpl); + SetWindowStartupLocation(owner?.PlatformImpl); - PlatformImpl?.Show(ShowActivated, false); - Renderer?.Start(); - OnOpened(EventArgs.Empty); + PlatformImpl?.Show(ShowActivated, false); + Renderer?.Start(); + OnOpened(EventArgs.Empty); + } } /// @@ -703,68 +750,66 @@ namespace Avalonia.Controls /// public Task ShowDialog(Window owner) { - if (owner == null) - { - throw new ArgumentNullException(nameof(owner)); - } - else if (owner.PlatformImpl == null) - { - throw new InvalidOperationException("Cannot show a window with a closed owner."); - } - else if (owner == this) - { - throw new InvalidOperationException("A Window cannot be its own owner."); - } - else if (IsVisible) - { - throw new InvalidOperationException("The window is already being shown."); - } - else if (!owner.IsVisible) + using (FreezeVisibilityChangeHandling()) { - throw new InvalidOperationException("Cannot show window with non-visible parent."); - } + EnsureStateBeforeShow(); + + if (owner == null) + { + throw new ArgumentNullException(nameof(owner)); + } + + EnsureParentStateBeforeShow(owner); - RaiseEvent(new RoutedEventArgs(WindowOpenedEvent)); + if (_shown) + { + throw new InvalidOperationException("The window is already being shown."); + } - EnsureInitialized(); - ApplyStyling(); - IsVisible = true; + RaiseEvent(new RoutedEventArgs(WindowOpenedEvent)); - var initialSize = new Size( - double.IsNaN(Width) ? ClientSize.Width : Width, - double.IsNaN(Height) ? ClientSize.Height : Height); + EnsureInitialized(); + ApplyStyling(); + _shown = true; + _showingAsDialog = true; + IsVisible = true; - if (initialSize != ClientSize) - { - PlatformImpl?.Resize(initialSize, PlatformResizeReason.Layout); - } + var initialSize = new Size( + double.IsNaN(Width) ? ClientSize.Width : Width, + double.IsNaN(Height) ? ClientSize.Height : Height); - LayoutManager.ExecuteInitialLayoutPass(); + if (initialSize != ClientSize) + { + PlatformImpl?.Resize(initialSize, PlatformResizeReason.Layout); + } - var result = new TaskCompletionSource(); + LayoutManager.ExecuteInitialLayoutPass(); - PlatformImpl?.SetParent(owner.PlatformImpl); - Owner = owner; - owner.AddChild(this, true); + var result = new TaskCompletionSource(); - SetWindowStartupLocation(owner.PlatformImpl); + PlatformImpl?.SetParent(owner.PlatformImpl!); + Owner = owner; + owner.AddChild(this, true); - PlatformImpl?.Show(ShowActivated, true); + SetWindowStartupLocation(owner.PlatformImpl); - Renderer?.Start(); + PlatformImpl?.Show(ShowActivated, true); - Observable.FromEventPattern( - x => Closed += x, - x => Closed -= x) - .Take(1) - .Subscribe(_ => - { - owner.Activate(); - result.SetResult((TResult)(_dialogResult ?? default(TResult)!)); - }); + Renderer?.Start(); - OnOpened(EventArgs.Empty); - return result.Task; + Observable.FromEventPattern( + x => Closed += x, + x => Closed -= x) + .Take(1) + .Subscribe(_ => + { + owner.Activate(); + result.SetResult((TResult)(_dialogResult ?? default(TResult)!)); + }); + + OnOpened(EventArgs.Empty); + return result.Task; + } } private void UpdateEnabled() diff --git a/src/Avalonia.Controls/WindowBase.cs b/src/Avalonia.Controls/WindowBase.cs index 8f1b2198ad..b71dc6df44 100644 --- a/src/Avalonia.Controls/WindowBase.cs +++ b/src/Avalonia.Controls/WindowBase.cs @@ -42,9 +42,11 @@ namespace Avalonia.Controls private bool _hasExecutedInitialLayoutPass; private bool _isActive; - private bool _ignoreVisibilityChange; + private int _ignoreVisibilityChanges; private WindowBase? _owner; + protected bool IgnoreVisibilityChanges => _ignoreVisibilityChanges > 0; + static WindowBase() { IsVisibleProperty.OverrideDefaultValue(false); @@ -66,6 +68,11 @@ namespace Avalonia.Controls impl.PositionChanged = HandlePositionChanged; } + protected IDisposable FreezeVisibilityChangeHandling() + { + return new IgnoreVisibilityChangesDisposable(this); + } + /// /// Fired when the window is activated. /// @@ -125,18 +132,12 @@ namespace Avalonia.Controls /// public virtual void Hide() { - _ignoreVisibilityChange = true; - - try + using (FreezeVisibilityChangeHandling()) { Renderer?.Stop(); PlatformImpl?.Hide(); IsVisible = false; } - finally - { - _ignoreVisibilityChange = false; - } } /// @@ -144,9 +145,7 @@ namespace Avalonia.Controls /// public virtual void Show() { - _ignoreVisibilityChange = true; - - try + using (FreezeVisibilityChangeHandling()) { EnsureInitialized(); ApplyStyling(); @@ -157,14 +156,11 @@ namespace Avalonia.Controls LayoutManager.ExecuteInitialLayoutPass(); _hasExecutedInitialLayoutPass = true; } + PlatformImpl?.Show(true, false); Renderer?.Start(); OnOpened(EventArgs.Empty); } - finally - { - _ignoreVisibilityChange = false; - } } /// @@ -202,23 +198,17 @@ namespace Avalonia.Controls protected override void HandleClosed() { - _ignoreVisibilityChange = true; - - try + using (FreezeVisibilityChangeHandling()) { IsVisible = false; - + if (this is IFocusScope scope) { FocusManager.Instance?.RemoveFocusScope(scope); } - + base.HandleClosed(); } - finally - { - _ignoreVisibilityChange = false; - } } /// @@ -318,9 +308,9 @@ namespace Avalonia.Controls Deactivated?.Invoke(this, EventArgs.Empty); } - private void IsVisibleChanged(AvaloniaPropertyChangedEventArgs e) + protected virtual void IsVisibleChanged(AvaloniaPropertyChangedEventArgs e) { - if (!_ignoreVisibilityChange) + if (_ignoreVisibilityChanges == 0) { if ((bool)e.NewValue!) { @@ -332,5 +322,21 @@ namespace Avalonia.Controls } } } + + private readonly struct IgnoreVisibilityChangesDisposable : IDisposable + { + private readonly WindowBase _windowBase; + + public IgnoreVisibilityChangesDisposable(WindowBase windowBase) + { + _windowBase = windowBase; + _windowBase._ignoreVisibilityChanges++; + } + + public void Dispose() + { + _windowBase._ignoreVisibilityChanges--; + } + } } } diff --git a/src/Avalonia.DesignerSupport/Remote/Stubs.cs b/src/Avalonia.DesignerSupport/Remote/Stubs.cs index 71389be1a6..80a4c7d897 100644 --- a/src/Avalonia.DesignerSupport/Remote/Stubs.cs +++ b/src/Avalonia.DesignerSupport/Remote/Stubs.cs @@ -61,7 +61,10 @@ namespace Avalonia.DesignerSupport.Remote })); } - public IRenderer CreateRenderer(IRenderRoot root) => new ImmediateRenderer((Visual)root); + public IRenderer CreateRenderer(IRenderRoot root) => new ImmediateRenderer((Visual)root, () => + new PlatformRenderInterfaceContextManager(null) + .CreateRenderTarget(Surfaces)); + public void Dispose() { } diff --git a/src/Avalonia.Diagnostics/Diagnostics/Controls/Application.cs b/src/Avalonia.Diagnostics/Diagnostics/Controls/Application.cs index 42c5ea1fa9..00173dbb35 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/Controls/Application.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/Controls/Application.cs @@ -10,8 +10,7 @@ namespace Avalonia.Diagnostics.Controls { private readonly App _application; - private static readonly Version s_version = typeof(AvaloniaObject).Assembly?.GetName()?.Version - ?? Version.Parse("0.0.00"); + public event EventHandler? Closed; public Application(App application) diff --git a/src/Avalonia.FreeDesktop/IX11InputMethod.cs b/src/Avalonia.FreeDesktop/IX11InputMethod.cs index 5d91118978..9fa9c1809e 100644 --- a/src/Avalonia.FreeDesktop/IX11InputMethod.cs +++ b/src/Avalonia.FreeDesktop/IX11InputMethod.cs @@ -11,7 +11,9 @@ namespace Avalonia.FreeDesktop (ITextInputMethodImpl method, IX11InputMethodControl control) CreateClient(IntPtr xid); } +#pragma warning disable CA1815 // Override equals and operator equals on value types public struct X11InputMethodForwardedKey +#pragma warning restore CA1815 // Override equals and operator equals on value types { public int KeyVal { get; set; } public KeyModifiers Modifiers { get; set; } diff --git a/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs b/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs index 7abc0ca131..9bb4b34976 100644 --- a/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs +++ b/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs @@ -12,7 +12,7 @@ using Avalonia.Media.Imaging; namespace Avalonia.Headless { - internal class HeadlessPlatformRenderInterface : IPlatformRenderInterface + internal class HeadlessPlatformRenderInterface : IPlatformRenderInterface, IPlatformRenderInterfaceContext { public static void Initialize() { @@ -22,6 +22,8 @@ namespace Avalonia.Headless public IEnumerable InstalledFontNames { get; } = new[] { "Tahoma" }; + public IPlatformRenderInterfaceContext CreateBackendContext(IPlatformGraphicsContext graphicsContext) => this; + public bool SupportsIndividualRoundRects => false; public AlphaFormat DefaultAlphaFormat => AlphaFormat.Premul; @@ -47,6 +49,7 @@ namespace Avalonia.Headless public IGeometryImpl CreateCombinedGeometry(GeometryCombineMode combineMode, Geometry g1, Geometry g2) => throw new NotImplementedException(); public IRenderTarget CreateRenderTarget(IEnumerable surfaces) => new HeadlessRenderTarget(); + public object TryGetFeature(Type featureType) => null; public IRenderTargetBitmapImpl CreateRenderTargetBitmap(PixelSize size, Vector dpi) { @@ -212,7 +215,7 @@ namespace Avalonia.Headless class HeadlessStreamingGeometryStub : HeadlessGeometryStub, IStreamGeometryImpl { - public HeadlessStreamingGeometryStub() : base(Rect.Empty) + public HeadlessStreamingGeometryStub() : base(default) { } @@ -313,6 +316,8 @@ namespace Avalonia.Headless return new HeadlessDrawingContextStub(); } + public bool IsCorrupted => false; + public void Blit(IDrawingContextImpl context) { @@ -474,6 +479,13 @@ namespace Avalonia.Headless { return new HeadlessDrawingContextStub(); } + + public bool IsCorrupted => false; + } + + public void Dispose() + { + } } } diff --git a/src/Avalonia.Headless/HeadlessPlatformStubs.cs b/src/Avalonia.Headless/HeadlessPlatformStubs.cs index 688b8e0398..1cc0fa73bb 100644 --- a/src/Avalonia.Headless/HeadlessPlatformStubs.cs +++ b/src/Avalonia.Headless/HeadlessPlatformStubs.cs @@ -145,13 +145,15 @@ namespace Avalonia.Headless class HeadlessTextShaperStub : ITextShaperImpl { - public ShapedBuffer ShapeText(ReadOnlySlice text, TextShaperOptions options) + public ShapedBuffer ShapeText(CharacterBufferReference text, int length, TextShaperOptions options) { var typeface = options.Typeface; var fontRenderingEmSize = options.FontRenderingEmSize; var bidiLevel = options.BidiLevel; - return new ShapedBuffer(text, text.Length, typeface, fontRenderingEmSize, bidiLevel); + var characterBufferRange = new CharacterBufferRange(text, length); + + return new ShapedBuffer(characterBufferRange, length, typeface, fontRenderingEmSize, bidiLevel); } } diff --git a/src/Avalonia.Headless/HeadlessWindowImpl.cs b/src/Avalonia.Headless/HeadlessWindowImpl.cs index 742df3324b..725fab1eaa 100644 --- a/src/Avalonia.Headless/HeadlessWindowImpl.cs +++ b/src/Avalonia.Headless/HeadlessWindowImpl.cs @@ -56,8 +56,9 @@ namespace Avalonia.Headless public IRenderer CreateRenderer(IRenderRoot root) => AvaloniaHeadlessPlatform.Compositor != null - ? new CompositingRenderer(root, AvaloniaHeadlessPlatform.Compositor) - : new DeferredRenderer(root, AvaloniaLocator.Current.GetRequiredService()); + ? new CompositingRenderer(root, AvaloniaHeadlessPlatform.Compositor, () => Surfaces) + : new DeferredRenderer(root, AvaloniaLocator.Current.GetRequiredService(), + () => new PlatformRenderInterfaceContextManager(null).CreateRenderTarget(Surfaces), null); public void Invalidate(Rect rect) { diff --git a/src/Avalonia.Native/AvaloniaNativePlatformOpenGlInterface.cs b/src/Avalonia.Native/AvaloniaNativeGlPlatformGraphics.cs similarity index 75% rename from src/Avalonia.Native/AvaloniaNativePlatformOpenGlInterface.cs rename to src/Avalonia.Native/AvaloniaNativeGlPlatformGraphics.cs index 14d27a90e9..8e40960af6 100644 --- a/src/Avalonia.Native/AvaloniaNativePlatformOpenGlInterface.cs +++ b/src/Avalonia.Native/AvaloniaNativeGlPlatformGraphics.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using Avalonia.OpenGL; using Avalonia.Native.Interop; using System.Drawing; @@ -8,18 +9,18 @@ using Avalonia.Threading; namespace Avalonia.Native { - class AvaloniaNativePlatformOpenGlInterface : IPlatformOpenGlInterface + class AvaloniaNativeGlPlatformGraphics : IPlatformGraphics { private readonly IAvnGlDisplay _display; - public AvaloniaNativePlatformOpenGlInterface(IAvnGlDisplay display) + public AvaloniaNativeGlPlatformGraphics(IAvnGlDisplay display) { _display = display; - var immediate = display.CreateContext(null); + var context = display.CreateContext(null); int major, minor; GlInterface glInterface; - using (immediate.MakeCurrent()) + using (context.MakeCurrent()) { var basic = new GlBasicInfoInterface(display.GetProcAddress); basic.GetIntegerv(GlConsts.GL_MAJOR_VERSION, out major); @@ -32,24 +33,23 @@ namespace Avalonia.Native }); } - GlDisplay = new GlDisplay(display, glInterface, immediate.SampleCount, immediate.StencilSize); - MainContext = new GlContext(GlDisplay, null, immediate, _version); + GlDisplay = new GlDisplay(display, glInterface, context.SampleCount, context.StencilSize); + SharedContext =(GlContext)CreateContext(); } - internal GlContext MainContext { get; } - public IGlContext PrimaryContext => MainContext; - IPlatformGpuContext IPlatformGpu.PrimaryContext => PrimaryContext; + + public bool UsesSharedContext => true; + public IPlatformGraphicsContext CreateContext() => new GlContext(GlDisplay, + null, _display.CreateContext(null), _version); + + public IPlatformGraphicsContext GetSharedContext() => SharedContext; + public bool CanShareContexts => true; public bool CanCreateContexts => true; internal GlDisplay GlDisplay; private readonly GlVersion _version; - - public IGlContext CreateSharedContext() => new GlContext(GlDisplay, - MainContext, _display.CreateContext(MainContext.Context), _version); - - public IGlContext CreateContext() => new GlContext(GlDisplay, - null, _display.CreateContext(null), _version); + internal GlContext SharedContext { get; } } class GlDisplay @@ -71,6 +71,10 @@ namespace Avalonia.Native public int StencilSize { get; } public void ClearContext() => _display.LegacyClearCurrentContext(); + + public GlContext CreateSharedContext(GlContext share) => + new GlContext(this, share, _display.CreateContext(share.Context), share.Version); + } class GlContext : IGlContext @@ -91,7 +95,14 @@ namespace Avalonia.Native public GlInterface GlInterface => _display.GlInterface; public int SampleCount => _display.SampleCount; public int StencilSize => _display.StencilSize; - public IDisposable MakeCurrent() => Context.MakeCurrent(); + public IDisposable MakeCurrent() + { + if (IsLost) + throw new PlatformGraphicsContextLostException(); + return Context.MakeCurrent(); + } + + public bool IsLost => Context == null; public IDisposable EnsureCurrent() => MakeCurrent(); public bool IsSharedWith(IGlContext context) @@ -103,12 +114,18 @@ namespace Avalonia.Native || _sharedWith != null && _sharedWith == c._sharedWith; } + public bool CanCreateSharedContext => true; + + public IGlContext CreateSharedContext(IEnumerable preferredVersions = null) => + _display.CreateSharedContext(_sharedWith ?? this); public void Dispose() { Context.Dispose(); Context = null; } + + public object TryGetFeature(Type featureType) => null; } @@ -125,7 +142,6 @@ namespace Avalonia.Native public IGlPlatformSurfaceRenderingSession BeginDraw() { - var feature = (AvaloniaNativePlatformOpenGlInterface)AvaloniaLocator.Current.GetService(); return new GlPlatformSurfaceRenderingSession(_context, _target.BeginDrawing()); } @@ -172,16 +188,14 @@ namespace Avalonia.Native class GlPlatformSurface : IGlPlatformSurface { private readonly IAvnWindowBase _window; - private readonly IGlContext _context; - - public GlPlatformSurface(IAvnWindowBase window, IGlContext context) + public GlPlatformSurface(IAvnWindowBase window) { _window = window; - _context = context; } - public IGlPlatformSurfaceRenderTarget CreateGlRenderTarget() + public IGlPlatformSurfaceRenderTarget CreateGlRenderTarget(IGlContext context) { - return new GlPlatformSurfaceRenderTarget(_window.CreateGlRenderTarget(), _context); + var avnContext = (GlContext)context; + return new GlPlatformSurfaceRenderTarget(_window.CreateGlRenderTarget(), avnContext); } } diff --git a/src/Avalonia.Native/AvaloniaNativePlatform.cs b/src/Avalonia.Native/AvaloniaNativePlatform.cs index db6b6f57cd..a5b2ea30cc 100644 --- a/src/Avalonia.Native/AvaloniaNativePlatform.cs +++ b/src/Avalonia.Native/AvaloniaNativePlatform.cs @@ -18,13 +18,14 @@ namespace Avalonia.Native { private readonly IAvaloniaNativeFactory _factory; private AvaloniaNativePlatformOptions _options; - private AvaloniaNativePlatformOpenGlInterface _platformGl; + private AvaloniaNativeGlPlatformGraphics _platformGl; [DllImport("libAvaloniaNative")] static extern IntPtr CreateAvaloniaNative(); internal static readonly KeyboardDevice KeyboardDevice = new KeyboardDevice(); [CanBeNull] internal static Compositor Compositor { get; private set; } + [CanBeNull] internal static PlatformRenderInterfaceContextManager RenderInterface { get; private set; } public static AvaloniaNativePlatform Initialize(IntPtr factory, AvaloniaNativePlatformOptions options) { @@ -126,10 +127,9 @@ namespace Avalonia.Native { try { - _platformGl = new AvaloniaNativePlatformOpenGlInterface(_factory.ObtainGlDisplay()); + _platformGl = new AvaloniaNativeGlPlatformGraphics(_factory.ObtainGlDisplay()); AvaloniaLocator.CurrentMutable - .Bind().ToConstant(_platformGl) - .Bind().ToConstant(_platformGl); + .Bind().ToConstant(_platformGl); } catch (Exception) @@ -137,12 +137,14 @@ namespace Avalonia.Native // ignored } } - + if (_options.UseDeferredRendering && _options.UseCompositor) { Compositor = new Compositor(renderLoop, _platformGl); } + else + RenderInterface = new PlatformRenderInterfaceContextManager(_platformGl); } public ITrayIconImpl CreateTrayIcon() diff --git a/src/Avalonia.Native/ClipboardImpl.cs b/src/Avalonia.Native/ClipboardImpl.cs index 4ee590516b..9f1c8883aa 100644 --- a/src/Avalonia.Native/ClipboardImpl.cs +++ b/src/Avalonia.Native/ClipboardImpl.cs @@ -15,8 +15,7 @@ namespace Avalonia.Native private IAvnClipboard _native; private const string NSPasteboardTypeString = "public.utf8-plain-text"; private const string NSFilenamesPboardType = "NSFilenamesPboardType"; - private const string NSPasteboardTypeFileUrl = "public.file-url"; - + public ClipboardImpl(IAvnClipboard native) { _native = native; diff --git a/src/Avalonia.Native/IAvnMenu.cs b/src/Avalonia.Native/IAvnMenu.cs index 3e46e0c5c6..e6a5a371d0 100644 --- a/src/Avalonia.Native/IAvnMenu.cs +++ b/src/Avalonia.Native/IAvnMenu.cs @@ -46,7 +46,6 @@ namespace Avalonia.Native.Interop.Impl private AvaloniaNativeMenuExporter _exporter; private List<__MicroComIAvnMenuItemProxy> _menuItems = new List<__MicroComIAvnMenuItemProxy>(); private Dictionary _menuItemLookup = new Dictionary(); - private CompositeDisposable _propertyDisposables = new CompositeDisposable(); public void RaiseNeedsUpdate() { diff --git a/src/Avalonia.Native/PopupImpl.cs b/src/Avalonia.Native/PopupImpl.cs index 76d3905b47..0953527284 100644 --- a/src/Avalonia.Native/PopupImpl.cs +++ b/src/Avalonia.Native/PopupImpl.cs @@ -8,12 +8,12 @@ namespace Avalonia.Native class PopupImpl : WindowBaseImpl, IPopupImpl { private readonly AvaloniaNativePlatformOptions _opts; - private readonly AvaloniaNativePlatformOpenGlInterface _glFeature; + private readonly AvaloniaNativeGlPlatformGraphics _glFeature; private readonly IWindowBaseImpl _parent; public PopupImpl(IAvaloniaNativeFactory factory, AvaloniaNativePlatformOptions opts, - AvaloniaNativePlatformOpenGlInterface glFeature, + AvaloniaNativeGlPlatformGraphics glFeature, IWindowBaseImpl parent) : base(factory, opts, glFeature) { _opts = opts; @@ -21,8 +21,7 @@ namespace Avalonia.Native _parent = parent; using (var e = new PopupEvents(this)) { - var context = _opts.UseGpu ? glFeature?.MainContext : null; - Init(factory.CreatePopup(e, context?.Context), factory.CreateScreens(), context); + Init(factory.CreatePopup(e, _glFeature.SharedContext.Context), factory.CreateScreens()); } PopupPositioner = new ManagedPopupPositioner(new ManagedPopupPositionerPopupImplHelper(parent, MoveResize)); } diff --git a/src/Avalonia.Native/WindowImpl.cs b/src/Avalonia.Native/WindowImpl.cs index 9de794ed53..2201503168 100644 --- a/src/Avalonia.Native/WindowImpl.cs +++ b/src/Avalonia.Native/WindowImpl.cs @@ -14,14 +14,14 @@ namespace Avalonia.Native internal class WindowImpl : WindowBaseImpl, IWindowImpl, ITopLevelImplWithNativeMenuExporter { private readonly AvaloniaNativePlatformOptions _opts; - private readonly AvaloniaNativePlatformOpenGlInterface _glFeature; + private readonly AvaloniaNativeGlPlatformGraphics _glFeature; IAvnWindow _native; private double _extendTitleBarHeight = -1; private DoubleClickHelper _doubleClickHelper; internal WindowImpl(IAvaloniaNativeFactory factory, AvaloniaNativePlatformOptions opts, - AvaloniaNativePlatformOpenGlInterface glFeature) : base(factory, opts, glFeature) + AvaloniaNativeGlPlatformGraphics glFeature) : base(factory, opts, glFeature) { _opts = opts; _glFeature = glFeature; @@ -29,8 +29,7 @@ namespace Avalonia.Native using (var e = new WindowEvents(this)) { - var context = _opts.UseGpu ? glFeature?.MainContext : null; - Init(_native = factory.CreateWindow(e, context?.Context), factory.CreateScreens(), context); + Init(_native = factory.CreateWindow(e, glFeature.SharedContext.Context), factory.CreateScreens()); } NativeMenuExporter = new AvaloniaNativeMenuExporter(_native, factory); diff --git a/src/Avalonia.Native/WindowImplBase.cs b/src/Avalonia.Native/WindowImplBase.cs index 4744a20107..ca57e30b1c 100644 --- a/src/Avalonia.Native/WindowImplBase.cs +++ b/src/Avalonia.Native/WindowImplBase.cs @@ -63,10 +63,9 @@ namespace Avalonia.Native private double _savedScaling; private GlPlatformSurface _glSurface; private NativeControlHostImpl _nativeControlHost; - private IGlContext _glContext; internal WindowBaseImpl(IAvaloniaNativeFactory factory, AvaloniaNativePlatformOptions opts, - AvaloniaNativePlatformOpenGlInterface glFeature) + AvaloniaNativeGlPlatformGraphics glFeature) { _factory = factory; _gpu = opts.UseGpu && glFeature != null; @@ -78,14 +77,13 @@ namespace Avalonia.Native StorageProvider = new SystemDialogs(this, _factory.CreateSystemDialogs()); } - protected void Init(IAvnWindowBase window, IAvnScreens screens, IGlContext glContext) + protected void Init(IAvnWindowBase window, IAvnScreens screens) { _native = window; - _glContext = glContext; Handle = new MacOSTopLevelWindowHandle(window); if (_gpu) - _glSurface = new GlPlatformSurface(window, _glContext); + _glSurface = new GlPlatformSurface(window); Screen = new ScreenImpl(screens); _savedLogicalSize = ClientSize; @@ -375,14 +373,17 @@ namespace Avalonia.Native if (_deferredRendering) { if (AvaloniaNativePlatform.Compositor != null) - return new CompositingRenderer(root, AvaloniaNativePlatform.Compositor) + return new CompositingRenderer(root, AvaloniaNativePlatform.Compositor, () => Surfaces) { RenderOnlyOnRenderThread = false }; - return new DeferredRenderer(root, loop); + return new DeferredRenderer(root, loop, + () => AvaloniaNativePlatform.RenderInterface!.CreateRenderTarget(Surfaces), + AvaloniaNativePlatform.RenderInterface); } - return new ImmediateRenderer((Visual)root); + return new ImmediateRenderer((Visual)root, + () => AvaloniaNativePlatform.RenderInterface!.CreateRenderTarget(Surfaces), AvaloniaNativePlatform.RenderInterface); } public virtual void Dispose() diff --git a/src/Avalonia.OpenGL/Angle/AngleWin32EglDisplay.cs b/src/Avalonia.OpenGL/Angle/AngleWin32EglDisplay.cs deleted file mode 100644 index 3a8fdb8491..0000000000 --- a/src/Avalonia.OpenGL/Angle/AngleWin32EglDisplay.cs +++ /dev/null @@ -1,95 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Runtime.InteropServices; -using Avalonia.OpenGL.Egl; -using static Avalonia.OpenGL.Egl.EglConsts; - -namespace Avalonia.OpenGL.Angle -{ - public class AngleWin32EglDisplay : EglDisplay - { - struct AngleInfo - { - public IntPtr Display { get; set; } - public AngleOptions.PlatformApi PlatformApi { get; set; } - } - - static AngleInfo CreateAngleDisplay(EglInterface _egl) - { - if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) - throw new PlatformNotSupportedException(); - var display = IntPtr.Zero; - AngleOptions.PlatformApi angleApi = default; - { - if (!_egl.IsGetPlatformDisplayExtAvailable) - throw new OpenGlException("eglGetPlatformDisplayEXT is not supported by libegl.dll"); - - var allowedApis = AvaloniaLocator.Current.GetService()?.AllowedPlatformApis - ?? new [] { AngleOptions.PlatformApi.DirectX11, AngleOptions.PlatformApi.DirectX9 }; - - foreach (var platformApi in allowedApis) - { - int dapi; - if (platformApi == AngleOptions.PlatformApi.DirectX9) - dapi = EGL_PLATFORM_ANGLE_TYPE_D3D9_ANGLE; - else if (platformApi == AngleOptions.PlatformApi.DirectX11) - dapi = EGL_PLATFORM_ANGLE_TYPE_D3D11_ANGLE; - else - continue; - - display = _egl.GetPlatformDisplayExt(EGL_PLATFORM_ANGLE_ANGLE, IntPtr.Zero, - new[] { EGL_PLATFORM_ANGLE_TYPE_ANGLE, dapi, EGL_NONE }); - if (display != IntPtr.Zero) - { - angleApi = platformApi; - break; - } - } - - if (display == IntPtr.Zero) - throw new OpenGlException("Unable to create ANGLE display"); - return new AngleInfo { Display = display, PlatformApi = angleApi }; - } - } - - private AngleWin32EglDisplay(EglInterface egl, AngleInfo info) : base(egl, false, info.Display) - { - PlatformApi = info.PlatformApi; - } - - public AngleWin32EglDisplay(EglInterface egl) : this(egl, CreateAngleDisplay(egl)) - { - - } - - public AngleWin32EglDisplay() : this(new AngleEglInterface()) - { - - } - - public AngleOptions.PlatformApi PlatformApi { get; } - - public IntPtr GetDirect3DDevice() - { - if (!EglInterface.QueryDisplayAttribExt(Handle, EglConsts.EGL_DEVICE_EXT, out var eglDevice)) - throw new OpenGlException("Unable to get EGL_DEVICE_EXT"); - if (!EglInterface.QueryDeviceAttribExt(eglDevice, PlatformApi == AngleOptions.PlatformApi.DirectX9 ? EGL_D3D9_DEVICE_ANGLE : EGL_D3D11_DEVICE_ANGLE, out var d3dDeviceHandle)) - throw new OpenGlException("Unable to get EGL_D3D9_DEVICE_ANGLE"); - return d3dDeviceHandle; - } - - public EglSurface WrapDirect3D11Texture(EglPlatformOpenGlInterface egl, IntPtr handle) - { - if (PlatformApi != AngleOptions.PlatformApi.DirectX11) - throw new InvalidOperationException("Current platform API is " + PlatformApi); - return egl.CreatePBufferFromClientBuffer(EGL_D3D_TEXTURE_ANGLE, handle, new[] { EGL_NONE, EGL_NONE }); - } - - public EglSurface WrapDirect3D11Texture(EglPlatformOpenGlInterface egl, IntPtr handle, int offsetX, int offsetY, int width, int height) - { - if (PlatformApi != AngleOptions.PlatformApi.DirectX11) - throw new InvalidOperationException("Current platform API is " + PlatformApi); - return egl.CreatePBufferFromClientBuffer(EGL_D3D_TEXTURE_ANGLE, handle, new[] { EGL_WIDTH, width, EGL_HEIGHT, height, EGL_FLEXIBLE_SURFACE_COMPATIBILITY_SUPPORTED_ANGLE, EGL_TRUE, EGL_TEXTURE_OFFSET_X_ANGLE, offsetX, EGL_TEXTURE_OFFSET_Y_ANGLE, offsetY, EGL_NONE }); - } - } -} diff --git a/src/Avalonia.OpenGL/Controls/OpenGlControlBase.cs b/src/Avalonia.OpenGL/Controls/OpenGlControlBase.cs index e13ee80864..c62a3d8d2f 100644 --- a/src/Avalonia.OpenGL/Controls/OpenGlControlBase.cs +++ b/src/Avalonia.OpenGL/Controls/OpenGlControlBase.cs @@ -1,8 +1,10 @@ using System; +using System.Threading.Tasks; using Avalonia.Controls; using Avalonia.Logging; using Avalonia.Media; using Avalonia.OpenGL.Imaging; +using Avalonia.VisualTree; using static Avalonia.OpenGL.GlConsts; namespace Avalonia.OpenGL.Controls @@ -14,8 +16,10 @@ namespace Avalonia.OpenGL.Controls private OpenGlBitmap _bitmap; private IOpenGlBitmapAttachment _attachment; private PixelSize _depthBufferSize; - private bool _glFailed; - private bool _initialized; + + private Task _initialization; + private IOpenGlTextureSharingRenderInterfaceContextFeature _feature; + protected GlVersion GlVersion { get; private set; } public sealed override void Render(DrawingContext context) { @@ -38,13 +42,6 @@ namespace Avalonia.OpenGL.Controls base.Render(context); } - private static void CheckError(GlInterface gl) - { - int err; - while ((err = gl.GetError()) != GL_NO_ERROR) - Console.WriteLine(err); - } - void EnsureTextureAttachment() { _context.GlInterface.BindFramebuffer(GL_FRAMEBUFFER, _fb); @@ -54,7 +51,7 @@ namespace Avalonia.OpenGL.Controls _attachment = null; _bitmap?.Dispose(); _bitmap = null; - _bitmap = new OpenGlBitmap(GetPixelSize(), new Vector(96, 96)); + _bitmap = new OpenGlBitmap(_feature, GetPixelSize(), new Vector(96, 96)); _attachment = _bitmap.CreateFramebufferAttachment(_context); } } @@ -87,9 +84,11 @@ namespace Avalonia.OpenGL.Controls gl.ActiveTexture(GL_TEXTURE0); gl.BindTexture(GL_TEXTURE_2D, 0); gl.BindFramebuffer(GL_FRAMEBUFFER, 0); - gl.DeleteFramebuffer(_fb); + if (_fb != 0) + gl.DeleteFramebuffer(_fb); _fb = 0; - gl.DeleteRenderbuffer(_depthBuffer); + if (_depthBuffer != 0) + gl.DeleteRenderbuffer(_depthBuffer); _depthBuffer = 0; _attachment?.Dispose(); _attachment = null; @@ -98,10 +97,10 @@ namespace Avalonia.OpenGL.Controls try { - if (_initialized) + if (_initialization is { Status: TaskStatus.RanToCompletion, Result: true }) { - _initialized = false; OnOpenGlDeinit(_context.GlInterface, _fb); + _initialization = null; } } finally @@ -111,6 +110,11 @@ namespace Avalonia.OpenGL.Controls } } } + + _fb = _depthBuffer = 0; + _attachment = null; + _bitmap = null; + _feature = null; } protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e) @@ -119,26 +123,12 @@ namespace Avalonia.OpenGL.Controls base.OnDetachedFromVisualTree(e); } - private bool EnsureInitializedCore() + private bool EnsureInitializedCore(IOpenGlTextureSharingRenderInterfaceContextFeature feature) { - if (_context != null) - return true; - - if (_glFailed) - return false; - - var feature = AvaloniaLocator.Current.GetService(); - if (feature == null) - return false; - if (!feature.CanShareContexts) - { - Logger.TryGet(LogEventLevel.Error, "OpenGL")?.Log("OpenGlControlBase", - "Unable to initialize OpenGL: current platform does not support multithreaded context sharing"); - return false; - } try { _context = feature.CreateSharedContext(); + _feature = feature; } catch (Exception e) { @@ -157,7 +147,7 @@ namespace Avalonia.OpenGL.Controls GlVersion = _context.Version; try { - _bitmap = new OpenGlBitmap(GetPixelSize(), new Vector(96, 96)); + _bitmap = new OpenGlBitmap(_feature, GetPixelSize(), new Vector(96, 96)); if (!_bitmap.SupportsContext(_context)) { Logger.TryGet(LogEventLevel.Error, "OpenGL")?.Log("OpenGlControlBase", @@ -212,15 +202,68 @@ namespace Avalonia.OpenGL.Controls return true; } + void ContextLost() + { + _context = null; + _feature = null; + _initialization = null; + _attachment = null; + _bitmap = null; + _fb = 0; + _depthBuffer = 0; + _depthBufferSize = default; + OnOpenGlLost(); + } + private bool EnsureInitialized() { - if (_initialized) - return true; - _glFailed = !(_initialized = EnsureInitializedCore()); - if (_glFailed) + if (_initialization != null) + { + // Check if we've previously failed to initialize OpenGL on this platform + if (_initialization is { IsCompleted: true, Result: false } || + _initialization?.IsFaulted == true) + return false; + + // Check if we are still waiting for init to complete + if (_initialization is { IsCompleted: false }) + return false; + + if (_context.IsLost) + ContextLost(); + else + return true; + } + + _initialization = InitializeAsync(); + return false; + + } + + private async Task InitializeAsync() + { + var contextSharingFeature = + (IOpenGlTextureSharingRenderInterfaceContextFeature) + await this.GetVisualRoot()!.Renderer.TryGetRenderInterfaceFeature( + typeof(IOpenGlTextureSharingRenderInterfaceContextFeature)); + + if (contextSharingFeature == null || !contextSharingFeature.CanCreateSharedContext) + { + Logger.TryGet(LogEventLevel.Error, "OpenGL")?.Log("OpenGlControlBase", + "Unable to initialize OpenGL: current platform does not support multithreaded context sharing"); return false; + } + + if (!EnsureInitializedCore(contextSharingFeature)) + { + DoCleanup(); + return false; + } + using (_context.MakeCurrent()) OnOpenGlInit(_context.GlInterface, _fb); + + InvalidateVisual(); + return true; } @@ -242,6 +285,11 @@ namespace Avalonia.OpenGL.Controls } + protected virtual void OnOpenGlLost() + { + + } + protected abstract void OnOpenGlRender(GlInterface gl, int fb); } } diff --git a/src/Avalonia.OpenGL/Egl/EglConsts.cs b/src/Avalonia.OpenGL/Egl/EglConsts.cs index 1268845f0f..428d11857f 100644 --- a/src/Avalonia.OpenGL/Egl/EglConsts.cs +++ b/src/Avalonia.OpenGL/Egl/EglConsts.cs @@ -196,6 +196,11 @@ namespace Avalonia.OpenGL.Egl // public const int EGL_PLATFORM_ANGLE_DEVICE_TYPE_D3D_WARP_ANGLE = 0x320B; // public const int EGL_PLATFORM_ANGLE_DEVICE_TYPE_D3D_REFERENCE_ANGLE = 0x320C; +// + // EXT_platform_device + public const int EGL_PLATFORM_DEVICE_EXT = 0x313F; + + //EXT_device_query public const int EGL_DEVICE_EXT = 0x322C; diff --git a/src/Avalonia.OpenGL/Egl/EglContext.cs b/src/Avalonia.OpenGL/Egl/EglContext.cs index e7b5dc7c83..4d75a776c3 100644 --- a/src/Avalonia.OpenGL/Egl/EglContext.cs +++ b/src/Avalonia.OpenGL/Egl/EglContext.cs @@ -1,6 +1,8 @@ using System; +using System.Collections.Generic; using System.Reactive.Disposables; using System.Threading; +using Avalonia.Platform; using static Avalonia.OpenGL.Egl.EglConsts; namespace Avalonia.OpenGL.Egl @@ -10,24 +12,32 @@ namespace Avalonia.OpenGL.Egl private readonly EglDisplay _disp; private readonly EglInterface _egl; private readonly EglContext _sharedWith; - private readonly object _lock = new object(); + private bool _isLost; + private IntPtr _context; + private readonly Action _disposeCallback; + private readonly Dictionary _features; + private readonly object _lock; - public EglContext(EglDisplay display, EglInterface egl, EglContext sharedWith, IntPtr ctx, Func offscreenSurface, - GlVersion version, int sampleCount, int stencilSize) + internal EglContext(EglDisplay display, EglInterface egl, EglContext sharedWith, IntPtr ctx, EglSurface offscreenSurface, + GlVersion version, int sampleCount, int stencilSize, Action disposeCallback, Dictionary features) { _disp = display; _egl = egl; _sharedWith = sharedWith; - Context = ctx; - OffscreenSurface = offscreenSurface(this); + _context = ctx; + _disposeCallback = disposeCallback; + _features = features; + OffscreenSurface = offscreenSurface; Version = version; SampleCount = sampleCount; StencilSize = stencilSize; + _lock = display.ContextSharedSyncRoot ?? new object(); using (MakeCurrent()) GlInterface = GlInterface.FromNativeUtf8GetProcAddress(version, _egl.GetProcAddress); } - public IntPtr Context { get; } + public IntPtr Context => + _context == IntPtr.Zero ? throw new ObjectDisposedException(nameof(EglContext)) : _context; public EglSurface OffscreenSurface { get; } public GlVersion Version { get; } public GlInterface GlInterface { get; } @@ -40,7 +50,7 @@ namespace Avalonia.OpenGL.Egl private readonly EglInterface _egl; private readonly object _l; private readonly IntPtr _display; - private IntPtr _context, _read, _draw; + public IntPtr _context, _read, _draw; public RestoreContext(EglInterface egl, IntPtr defDisplay, object l) { @@ -66,6 +76,9 @@ namespace Avalonia.OpenGL.Egl public IDisposable MakeCurrent(EglSurface surface) { + if (IsLost) + throw new PlatformGraphicsContextLostException(); + Monitor.Enter(_lock); var success = false; try @@ -74,8 +87,18 @@ namespace Avalonia.OpenGL.Egl var surf = surface ?? OffscreenSurface; _egl.MakeCurrent(_disp.Handle, IntPtr.Zero, IntPtr.Zero, IntPtr.Zero); if (!_egl.MakeCurrent(_disp.Handle, surf?.DangerousGetHandle() ?? IntPtr.Zero, - surf?.DangerousGetHandle() ?? IntPtr.Zero, Context)) - throw OpenGlException.GetFormattedException("eglMakeCurrent", _egl); + surf?.DangerousGetHandle() ?? IntPtr.Zero, Context)) + { + var error = _egl.GetError(); + if (error == EGL_CONTEXT_LOST) + { + NotifyContextLost(); + throw new PlatformGraphicsContextLostException(); + } + + throw OpenGlException.GetFormattedEglException("eglMakeCurrent", error); + } + success = true; return old; } @@ -85,7 +108,15 @@ namespace Avalonia.OpenGL.Egl Monitor.Exit(_lock); } } + + public void NotifyContextLost() + { + _isLost = true; + _disp.OnContextLost(this); + } + public bool IsLost => _isLost || _disp.IsLost || Context == IntPtr.Zero; + public IDisposable EnsureCurrent() { if(IsCurrent) @@ -110,12 +141,32 @@ namespace Avalonia.OpenGL.Egl || _sharedWith != null && _sharedWith == c._sharedWith; } + public bool CanCreateSharedContext => _disp.SupportsSharing; + + public IGlContext CreateSharedContext(IEnumerable preferredVersions = null) => + _disp.CreateContext(new EglContextOptions + { + ShareWith = _sharedWith ?? this + }); + public bool IsCurrent => _egl.GetCurrentDisplay() == _disp.Handle && _egl.GetCurrentContext() == Context; public void Dispose() { + if(_context == IntPtr.Zero) + return; _egl.DestroyContext(_disp.Handle, Context); OffscreenSurface?.Dispose(); + _context = IntPtr.Zero; + _disp.OnContextDisposed(this); + _disposeCallback?.Invoke(); + } + + public object TryGetFeature(Type featureType) + { + if (_features?.TryGetValue(featureType, out var feature) == true) + return feature; + return null; } } } diff --git a/src/Avalonia.OpenGL/Egl/EglDisplay.cs b/src/Avalonia.OpenGL/Egl/EglDisplay.cs index d3d85e8c83..eea2587587 100644 --- a/src/Avalonia.OpenGL/Egl/EglDisplay.cs +++ b/src/Avalonia.OpenGL/Egl/EglDisplay.cs @@ -1,193 +1,179 @@ using System; +using System.Collections.Generic; using System.Linq; +using System.Reactive.Disposables; +using System.Threading; using static Avalonia.OpenGL.Egl.EglConsts; namespace Avalonia.OpenGL.Egl { - public class EglDisplay + public class EglDisplay : IDisposable { private readonly EglInterface _egl; + private IntPtr _display; + private readonly EglDisplayOptions _options; + private EglConfigInfo _config; + private bool _isLost; + private object _lock = new(); + public bool SupportsSharing { get; } - private readonly IntPtr _display; - private readonly IntPtr _config; - private readonly int[] _contextAttributes; - private readonly int _surfaceType; public IntPtr Handle => _display; - public IntPtr Config => _config; - private int _sampleCount; - private int _stencilSize; - private GlVersion _version; - - public EglDisplay(EglInterface egl, bool supportsSharing) : this(egl, supportsSharing, -1, IntPtr.Zero, null) + public IntPtr Config => _config.Config; + internal bool SingleContext => !_options.SupportsMultipleContexts; + private List _contexts = new(); + + public EglDisplay() : this(new EglDisplayCreationOptions { - - } - - static IntPtr CreateDisplay(EglInterface egl, int platformType, IntPtr platformDisplay, int[] attrs) + Egl = new EglInterface() + }) { - var display = IntPtr.Zero; - if (platformType == -1 && platformDisplay == IntPtr.Zero) - { - if (display == IntPtr.Zero) - display = egl.GetDisplay(IntPtr.Zero); - } - else - { - if (!egl.IsGetPlatformDisplayExtAvailable) - throw new OpenGlException("eglGetPlatformDisplayEXT is not supported by libegl"); - display = egl.GetPlatformDisplayExt(platformType, platformDisplay, attrs); - } - - if (display == IntPtr.Zero) - throw OpenGlException.GetFormattedException("eglGetDisplay", egl); - return display; + } - public EglDisplay(EglInterface egl, bool supportsSharing, int platformType, IntPtr platformDisplay, int[] attrs) - : this(egl, supportsSharing, CreateDisplay(egl, platformType, platformDisplay, attrs)) + public EglDisplay(EglDisplayCreationOptions options) : this(EglDisplayUtils.CreateDisplay(options), options) { - + } - - public EglDisplay(EglInterface egl, bool supportsSharing, IntPtr display) + + public EglDisplay(IntPtr display, EglDisplayOptions options) { - _egl = egl; - SupportsSharing = supportsSharing; + _egl = options.Egl; + SupportsSharing = options.SupportsContextSharing; _display = display; + _options = options; if(_display == IntPtr.Zero) throw new ArgumentException(); - - - if (!_egl.Initialize(_display, out var major, out var minor)) - throw OpenGlException.GetFormattedException("eglInitialize", _egl); - - var glProfiles = AvaloniaLocator.Current.GetService()?.GlProfiles - ?? new[] - { - new GlVersion(GlProfileType.OpenGLES, 3, 0), - new GlVersion(GlProfileType.OpenGLES, 2, 0) - }; - var cfgs = glProfiles.Select(x => + _config = EglDisplayUtils.InitializeAndGetConfig(_egl, display, options.GlVersions); + } + + public EglInterface EglInterface => _egl; + public EglContext CreateContext(EglContextOptions options) + { + if (SingleContext && _contexts.Any()) + throw new OpenGlException("This EGLDisplay can only have one active context"); + + options ??= new EglContextOptions(); + lock (_lock) { - var typeBit = EGL_OPENGL_ES3_BIT; + var share = options.ShareWith; + if (share != null && !SupportsSharing) + throw new NotSupportedException("Context sharing is not supported by this display"); - switch (x.Major) - { - case 2: - typeBit = EGL_OPENGL_ES2_BIT; - break; + var offscreenSurface = options.OffscreenSurface; - case 1: - typeBit = EGL_OPENGL_ES_BIT; - break; - } - - return new + if (offscreenSurface == null) { - Attributes = new[] + // Check if eglMakeCurrent can work with EGL_NONE as read-write surfaces + var extensions = _egl.QueryString(Handle, EGL_EXTENSIONS); + if (extensions?.Contains("EGL_KHR_surfaceless_context") != true) { - EGL_CONTEXT_MAJOR_VERSION, x.Major, - EGL_CONTEXT_MINOR_VERSION, x.Minor, - EGL_NONE - }, - Api = EGL_OPENGL_ES_API, - RenderableTypeBit = typeBit, - Version = x - }; - }); - - foreach (var cfg in cfgs) - { - if (!_egl.BindApi(cfg.Api)) - continue; - foreach(var surfaceType in new[]{EGL_PBUFFER_BIT|EGL_WINDOW_BIT, EGL_WINDOW_BIT}) - foreach(var stencilSize in new[]{8, 1, 0}) - foreach (var depthSize in new []{8, 1, 0}) + // Attempt to create a PBuffer as a surface for offscreen rendering + if ((_config.SurfaceType | EGL_PBUFFER_BIT) == 0) + throw new InvalidOperationException( + "Platform doesn't support EGL_KHR_surfaceless_context and PBUFFER surfaces"); + + var pBufferSurface = _egl.CreatePBufferSurface(_display, Config, + new[] { EGL_WIDTH, 1, EGL_HEIGHT, 1, EGL_NONE }); + if (pBufferSurface == IntPtr.Zero) + throw OpenGlException.GetFormattedException("eglCreatePBufferSurface", _egl); + + offscreenSurface = new EglSurface(this, pBufferSurface); + } + } + + var ctx = _egl.CreateContext(_display, Config, share?.Context ?? IntPtr.Zero, _config.Attributes); + if (ctx == IntPtr.Zero) { - var attribs = new[] - { - EGL_SURFACE_TYPE, surfaceType, - EGL_RENDERABLE_TYPE, cfg.RenderableTypeBit, - EGL_RED_SIZE, 8, - EGL_GREEN_SIZE, 8, - EGL_BLUE_SIZE, 8, - EGL_ALPHA_SIZE, 8, - EGL_STENCIL_SIZE, stencilSize, - EGL_DEPTH_SIZE, depthSize, - EGL_NONE - }; - if (!_egl.ChooseConfig(_display, attribs, out _config, 1, out int numConfigs)) - continue; - if (numConfigs == 0) - continue; - _contextAttributes = cfg.Attributes; - _surfaceType = surfaceType; - _version = cfg.Version; - _egl.GetConfigAttrib(_display, _config, EGL_SAMPLES, out _sampleCount); - _egl.GetConfigAttrib(_display, _config, EGL_STENCIL_SIZE, out _stencilSize); - goto Found; + var ex = OpenGlException.GetFormattedException("eglCreateContext", _egl); + offscreenSurface?.Dispose(); + throw ex; } + var rv = new EglContext(this, _egl, share, ctx, offscreenSurface, + _config.Version, _config.SampleCount, _config.StencilSize, + options.DisposeCallback, options.ExtraFeatures); + _contexts.Add(rv); + return rv; } - Found: - if (_contextAttributes == null) - throw new OpenGlException("No suitable EGL config was found"); } - public EglDisplay() : this(false) + public EglSurface CreateWindowSurface(IntPtr window) { - + if (window == IntPtr.Zero) + throw new OpenGlException($"Window {window} is invalid."); + + using (Lock()) + { + var s = EglInterface.CreateWindowSurface(Handle, Config, window, + new[] { EGL_NONE, EGL_NONE }); + if (s == IntPtr.Zero) + throw OpenGlException.GetFormattedException("eglCreateWindowSurface", EglInterface); + return new EglSurface(this, s); + } } - public EglDisplay(bool supportsSharing) : this(new EglInterface(), supportsSharing) + public EglSurface CreatePBufferFromClientBuffer (int bufferType, IntPtr handle, int[] attribs) { - + using (Lock()) + { + var s = EglInterface.CreatePbufferFromClientBuffer(Handle, bufferType, handle, + Config, attribs); + + if (s == IntPtr.Zero) + throw OpenGlException.GetFormattedException("eglCreatePbufferFromClientBuffer", EglInterface); + return new EglSurface(this, s); + } } + + protected virtual bool DisplayLockIsSharedWithContexts => false; - public EglInterface EglInterface => _egl; - public EglContext CreateContext(IGlContext share) + internal object ContextSharedSyncRoot => DisplayLockIsSharedWithContexts ? _lock : null; + + internal void OnContextLost(EglContext context) { - if (share != null && !SupportsSharing) - throw new NotSupportedException("Context sharing is not supported by this display"); - - if((_surfaceType|EGL_PBUFFER_BIT) == 0) - throw new InvalidOperationException("Platform doesn't support PBUFFER surfaces"); - var shareCtx = (EglContext)share; - var ctx = _egl.CreateContext(_display, _config, shareCtx?.Context ?? IntPtr.Zero, _contextAttributes); - if (ctx == IntPtr.Zero) - throw OpenGlException.GetFormattedException("eglCreateContext", _egl); - - var extensions = _egl.QueryString(Handle, EGL_EXTENSIONS); - - IntPtr surf = IntPtr.Zero; - if (extensions?.Contains("EGL_KHR_surfaceless_context") != true) + if (_options.ContextLossIsDisplayLoss) + _isLost = true; + } + + internal void OnContextDisposed(EglContext context) + { + lock (_lock) + _contexts.Remove(context); + } + + public bool IsLost + { + get { - surf = _egl.CreatePBufferSurface(_display, _config, - new[] { EGL_WIDTH, 1, EGL_HEIGHT, 1, EGL_NONE }); - if (surf == IntPtr.Zero) - throw OpenGlException.GetFormattedException("eglCreatePBufferSurface", _egl); + if (_isLost || _display == IntPtr.Zero) + return true; + if (_options.DeviceLostCheckCallback?.Invoke() == true) + return _isLost = true; + return false; } - - var rv = new EglContext(this, _egl, shareCtx, ctx, - context => - surf == IntPtr.Zero ? null : new EglSurface(this, context, surf), - _version, _sampleCount, _stencilSize); - return rv; + } + + public IDisposable Lock() + { + Monitor.Enter(_lock); + return Disposable.Create(() => { Monitor.Exit(_lock); }); } - public EglContext CreateContext(EglContext share, EglSurface offscreenSurface) + public void Dispose() { - if (share != null && !SupportsSharing) - throw new NotSupportedException("Context sharing is not supported by this display"); - - var ctx = _egl.CreateContext(_display, _config, share?.Context ?? IntPtr.Zero, _contextAttributes); - if (ctx == IntPtr.Zero) - throw OpenGlException.GetFormattedException("eglCreateContext", _egl); - var rv = new EglContext(this, _egl, share, ctx, _ => offscreenSurface, _version, _sampleCount, _stencilSize); - rv.MakeCurrent(null); - return rv; + lock (_lock) + { + foreach(var ctx in _contexts) + ctx.Dispose(); + _contexts.Clear(); + if (_display != IntPtr.Zero) + _egl.Terminate(_display); + _display = IntPtr.Zero; + _config = null; + _options.DisposeCallback?.Invoke(); + } } } } diff --git a/src/Avalonia.OpenGL/Egl/EglDisplayOptions.cs b/src/Avalonia.OpenGL/Egl/EglDisplayOptions.cs new file mode 100644 index 0000000000..5648645c54 --- /dev/null +++ b/src/Avalonia.OpenGL/Egl/EglDisplayOptions.cs @@ -0,0 +1,30 @@ +using System; +using System.Collections.Generic; + +namespace Avalonia.OpenGL.Egl; + +public class EglDisplayOptions +{ + public EglInterface Egl { get; set; } + public bool SupportsContextSharing { get; set; } + public bool SupportsMultipleContexts { get; set; } + public bool ContextLossIsDisplayLoss { get; set; } + public Func DeviceLostCheckCallback { get; set; } + public Action DisposeCallback { get; set; } + public IEnumerable GlVersions { get; set; } +} + +public class EglContextOptions +{ + public EglContext ShareWith { get; set; } + public EglSurface OffscreenSurface { get; set; } + public Action DisposeCallback { get; set; } + public Dictionary ExtraFeatures { get; set; } +} + +public class EglDisplayCreationOptions : EglDisplayOptions +{ + public int? PlatformType { get; set; } + public IntPtr PlatformDisplay { get; set; } + public int[] PlatformDisplayAttrs { get; set; } +} diff --git a/src/Avalonia.OpenGL/Egl/EglDisplayUtils.cs b/src/Avalonia.OpenGL/Egl/EglDisplayUtils.cs new file mode 100644 index 0000000000..fbfaf1bd3d --- /dev/null +++ b/src/Avalonia.OpenGL/Egl/EglDisplayUtils.cs @@ -0,0 +1,135 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reactive.Disposables; +using System.Threading; +using static Avalonia.OpenGL.Egl.EglConsts; +namespace Avalonia.OpenGL.Egl; + +static class EglDisplayUtils +{ + public static IntPtr CreateDisplay(EglDisplayCreationOptions options) + { + var egl = options.Egl; + var display = IntPtr.Zero; + if (options.PlatformType == null) + { + if (display == IntPtr.Zero) + display = egl.GetDisplay(IntPtr.Zero); + } + else + { + if (!egl.IsGetPlatformDisplayExtAvailable) + throw new OpenGlException("eglGetPlatformDisplayEXT is not supported by libegl"); + + display = egl.GetPlatformDisplayExt(options.PlatformType.Value, options.PlatformDisplay, + options.PlatformDisplayAttrs); + } + + if (display == IntPtr.Zero) + throw OpenGlException.GetFormattedException("eglGetDisplay", egl); + return display; + } + + public static EglConfigInfo InitializeAndGetConfig(EglInterface egl, IntPtr display, IEnumerable versions) + { + if (!egl.Initialize(display, out var major, out var minor)) + throw OpenGlException.GetFormattedException("eglInitialize", egl); + + // TODO: AvaloniaLocator.Current.GetService()?.GlProfiles + versions ??= new[] + { + new GlVersion(GlProfileType.OpenGLES, 3, 0), + new GlVersion(GlProfileType.OpenGLES, 2, 0) + }; + + var cfgs = versions + .Where(x => x.Type == GlProfileType.OpenGLES) + .Select(x => + { + var typeBit = EGL_OPENGL_ES3_BIT; + + switch (x.Major) + { + case 2: + typeBit = EGL_OPENGL_ES2_BIT; + break; + + case 1: + typeBit = EGL_OPENGL_ES_BIT; + break; + } + + return new + { + Attributes = new[] + { + EGL_CONTEXT_MAJOR_VERSION, x.Major, + EGL_CONTEXT_MINOR_VERSION, x.Minor, + EGL_NONE + }, + Api = EGL_OPENGL_ES_API, + RenderableTypeBit = typeBit, + Version = x + }; + }); + + foreach (var cfg in cfgs) + { + if (!egl.BindApi(cfg.Api)) + continue; + foreach (var surfaceType in new[] { EGL_PBUFFER_BIT | EGL_WINDOW_BIT, EGL_WINDOW_BIT }) + foreach (var stencilSize in new[] { 8, 1, 0 }) + foreach (var depthSize in new[] { 8, 1, 0 }) + { + var attribs = new[] + { + EGL_SURFACE_TYPE, surfaceType, + EGL_RENDERABLE_TYPE, cfg.RenderableTypeBit, + EGL_RED_SIZE, 8, + EGL_GREEN_SIZE, 8, + EGL_BLUE_SIZE, 8, + EGL_ALPHA_SIZE, 8, + EGL_STENCIL_SIZE, stencilSize, + EGL_DEPTH_SIZE, depthSize, + EGL_NONE + }; + if (!egl.ChooseConfig(display, attribs, out var config, 1, out int numConfigs)) + continue; + if (numConfigs == 0) + continue; + + + egl.GetConfigAttrib(display, config, EGL_SAMPLES, out var sampleCount); + egl.GetConfigAttrib(display, config, EGL_STENCIL_SIZE, out var returnedStencilSize); + return new EglConfigInfo(config, cfg.Version, surfaceType, cfg.Attributes, sampleCount, + returnedStencilSize); + } + } + + throw new OpenGlException("No suitable EGL config was found"); + } + + +} + +class EglConfigInfo +{ + public IntPtr Config { get; } + public GlVersion Version { get; } + public int SurfaceType { get; } + public int[] Attributes { get; } + public int SampleCount { get; } + public int StencilSize { get; } + + public EglConfigInfo(IntPtr config, GlVersion version, int surfaceType, int[] attributes, int sampleCount, + int stencilSize) + { + Config = config; + Version = version; + SurfaceType = surfaceType; + Attributes = attributes; + SampleCount = sampleCount; + StencilSize = stencilSize; + } +} \ No newline at end of file diff --git a/src/Avalonia.OpenGL/Egl/EglGlPlatformSurface.cs b/src/Avalonia.OpenGL/Egl/EglGlPlatformSurface.cs index 3d58660d47..348028bd08 100644 --- a/src/Avalonia.OpenGL/Egl/EglGlPlatformSurface.cs +++ b/src/Avalonia.OpenGL/Egl/EglGlPlatformSurface.cs @@ -1,52 +1,61 @@ +using System; using Avalonia.OpenGL.Surfaces; namespace Avalonia.OpenGL.Egl { public class EglGlPlatformSurface : EglGlPlatformSurfaceBase { - private readonly EglPlatformOpenGlInterface _egl; + public interface IEglWindowGlPlatformSurfaceInfo + { + IntPtr Handle { get; } + PixelSize Size { get; } + double Scaling { get; } + } + private readonly IEglWindowGlPlatformSurfaceInfo _info; - public EglGlPlatformSurface(EglPlatformOpenGlInterface egl, IEglWindowGlPlatformSurfaceInfo info) : base() + public EglGlPlatformSurface(IEglWindowGlPlatformSurfaceInfo info) { - _egl = egl; _info = info; } - public override IGlPlatformSurfaceRenderTarget CreateGlRenderTarget() + public override IGlPlatformSurfaceRenderTarget CreateGlRenderTarget(IGlContext context) { - var glSurface = _egl.CreateWindowSurface(_info.Handle); - return new RenderTarget(_egl, glSurface, _info); + var eglContext = (EglContext)context; + + var glSurface = eglContext.Display.CreateWindowSurface(_info.Handle); + return new RenderTarget(glSurface, eglContext, _info); } class RenderTarget : EglPlatformSurfaceRenderTargetBase { - private readonly EglPlatformOpenGlInterface _egl; private EglSurface _glSurface; private readonly IEglWindowGlPlatformSurfaceInfo _info; private PixelSize _currentSize; + private readonly IntPtr _handle; - public RenderTarget(EglPlatformOpenGlInterface egl, - EglSurface glSurface, IEglWindowGlPlatformSurfaceInfo info) : base(egl) + public RenderTarget(EglSurface glSurface, EglContext context, IEglWindowGlPlatformSurfaceInfo info) : base(context) { - _egl = egl; _glSurface = glSurface; _info = info; _currentSize = info.Size; + _handle = _info.Handle; } public override void Dispose() => _glSurface.Dispose(); - - public override IGlPlatformSurfaceRenderingSession BeginDraw() + + public override IGlPlatformSurfaceRenderingSession BeginDrawCore() { - if (_info.Size != _currentSize || _glSurface == null) + if (_info.Size != _currentSize + || _handle != _info.Handle + || _glSurface == null) { _glSurface?.Dispose(); _glSurface = null; - _glSurface = _egl.CreateWindowSurface(_info.Handle); + _glSurface = Context.Display.CreateWindowSurface(_info.Handle); _currentSize = _info.Size; } - return base.BeginDraw(_glSurface, _info); + return base.BeginDraw(_glSurface, _info.Size, _info.Scaling); } } } diff --git a/src/Avalonia.OpenGL/Egl/EglGlPlatformSurfaceBase.cs b/src/Avalonia.OpenGL/Egl/EglGlPlatformSurfaceBase.cs index 4ea6766de2..f66f630556 100644 --- a/src/Avalonia.OpenGL/Egl/EglGlPlatformSurfaceBase.cs +++ b/src/Avalonia.OpenGL/Egl/EglGlPlatformSurfaceBase.cs @@ -5,23 +5,16 @@ namespace Avalonia.OpenGL.Egl { public abstract class EglGlPlatformSurfaceBase : IGlPlatformSurface { - public interface IEglWindowGlPlatformSurfaceInfo - { - IntPtr Handle { get; } - PixelSize Size { get; } - double Scaling { get; } - } - - public abstract IGlPlatformSurfaceRenderTarget CreateGlRenderTarget(); + public abstract IGlPlatformSurfaceRenderTarget CreateGlRenderTarget(IGlContext context); } - public abstract class EglPlatformSurfaceRenderTargetBase : IGlPlatformSurfaceRenderTarget + public abstract class EglPlatformSurfaceRenderTargetBase : IGlPlatformSurfaceRenderTargetWithCorruptionInfo { - private readonly EglPlatformOpenGlInterface _egl; + protected EglContext Context { get; } - protected EglPlatformSurfaceRenderTargetBase(EglPlatformOpenGlInterface egl) + protected EglPlatformSurfaceRenderTargetBase(EglContext context) { - _egl = egl; + Context = context; } public virtual void Dispose() @@ -29,25 +22,33 @@ namespace Avalonia.OpenGL.Egl } - public abstract IGlPlatformSurfaceRenderingSession BeginDraw(); + public IGlPlatformSurfaceRenderingSession BeginDraw() + { + if (Context.IsLost) + throw new RenderTargetCorruptedException(); + + return BeginDrawCore(); + } + + public abstract IGlPlatformSurfaceRenderingSession BeginDrawCore(); protected IGlPlatformSurfaceRenderingSession BeginDraw(EglSurface surface, - EglGlPlatformSurfaceBase.IEglWindowGlPlatformSurfaceInfo info, Action onFinish = null, bool isYFlipped = false) + PixelSize size, double scaling, Action onFinish = null, bool isYFlipped = false) { - var restoreContext = _egl.PrimaryEglContext.MakeCurrent(surface); + var restoreContext = Context.MakeCurrent(surface); var success = false; try { - var egli = _egl.Display.EglInterface; + var egli = Context.Display.EglInterface; egli.WaitClient(); egli.WaitGL(); egli.WaitNative(EglConsts.EGL_CORE_NATIVE_ENGINE); - _egl.PrimaryContext.GlInterface.BindFramebuffer(GlConsts.GL_FRAMEBUFFER, 0); + Context.GlInterface.BindFramebuffer(GlConsts.GL_FRAMEBUFFER, 0); success = true; - return new Session(_egl.Display, _egl.PrimaryEglContext, surface, info, restoreContext, onFinish, isYFlipped); + return new Session(Context.Display, Context, surface, size, scaling, restoreContext, onFinish, isYFlipped); } finally { @@ -60,21 +61,21 @@ namespace Avalonia.OpenGL.Egl { private readonly EglContext _context; private readonly EglSurface _glSurface; - private readonly EglGlPlatformSurfaceBase.IEglWindowGlPlatformSurfaceInfo _info; private readonly EglDisplay _display; private readonly IDisposable _restoreContext; private readonly Action _onFinish; public Session(EglDisplay display, EglContext context, - EglSurface glSurface, EglGlPlatformSurfaceBase.IEglWindowGlPlatformSurfaceInfo info, + EglSurface glSurface, PixelSize size, double scaling, IDisposable restoreContext, Action onFinish, bool isYFlipped) { + Size = size; + Scaling = scaling; IsYFlipped = isYFlipped; _context = context; _display = display; _glSurface = glSurface; - _info = info; _restoreContext = restoreContext; _onFinish = onFinish; } @@ -92,9 +93,11 @@ namespace Avalonia.OpenGL.Egl } public IGlContext Context => _context; - public PixelSize Size => _info.Size; - public double Scaling => _info.Scaling; + public PixelSize Size { get; } + public double Scaling { get; } public bool IsYFlipped { get; } } + + public virtual bool IsCorrupted => Context.IsLost; } } diff --git a/src/Avalonia.OpenGL/Egl/EglInterface.cs b/src/Avalonia.OpenGL/Egl/EglInterface.cs index 4fec8e5356..ad4b55a686 100644 --- a/src/Avalonia.OpenGL/Egl/EglInterface.cs +++ b/src/Avalonia.OpenGL/Egl/EglInterface.cs @@ -53,6 +53,9 @@ namespace Avalonia.OpenGL.Egl [GetProcAddress("eglInitialize")] public partial bool Initialize(IntPtr display, out int major, out int minor); + + [GetProcAddress("eglTerminate")] + public partial void Terminate(IntPtr display); [GetProcAddress("eglGetProcAddress")] public partial IntPtr GetProcAddress(IntPtr proc); diff --git a/src/Avalonia.OpenGL/Egl/EglPlatformGraphics.cs b/src/Avalonia.OpenGL/Egl/EglPlatformGraphics.cs new file mode 100644 index 0000000000..faa9f279a6 --- /dev/null +++ b/src/Avalonia.OpenGL/Egl/EglPlatformGraphics.cs @@ -0,0 +1,48 @@ +using System; +using Avalonia.Logging; +using Avalonia.Platform; +using static Avalonia.OpenGL.Egl.EglConsts; + +namespace Avalonia.OpenGL.Egl +{ + public sealed class EglPlatformGraphics : IPlatformGraphics + { + private readonly EglDisplay _display; + public bool UsesSharedContext => false; + public IPlatformGraphicsContext CreateContext() => _display.CreateContext(null); + public IPlatformGraphicsContext GetSharedContext() => throw new NotSupportedException(); + + public EglPlatformGraphics(EglDisplay display) + { + _display = display; + } + + public static void TryInitialize() + { + var feature = TryCreate(); + if (feature != null) + AvaloniaLocator.CurrentMutable.Bind().ToConstant(feature); + } + + public static EglPlatformGraphics TryCreate() => TryCreate(() => new EglDisplay(new EglDisplayCreationOptions + { + Egl = new EglInterface(), + // Those are expected to be supported by most EGL implementations + SupportsMultipleContexts = true, + SupportsContextSharing = true + })); + + public static EglPlatformGraphics TryCreate(Func displayFactory) + { + try + { + return new EglPlatformGraphics(displayFactory()); + } + catch(Exception e) + { + Logger.TryGet(LogEventLevel.Error, "OpenGL")?.Log(null, "Unable to initialize EGL-based rendering: {0}", e); + return null; + } + } + } +} diff --git a/src/Avalonia.OpenGL/Egl/EglPlatformOpenGlInterface.cs b/src/Avalonia.OpenGL/Egl/EglPlatformOpenGlInterface.cs deleted file mode 100644 index a1ac2a9d37..0000000000 --- a/src/Avalonia.OpenGL/Egl/EglPlatformOpenGlInterface.cs +++ /dev/null @@ -1,77 +0,0 @@ -using System; -using Avalonia.Logging; -using Avalonia.Platform; -using static Avalonia.OpenGL.Egl.EglConsts; - -namespace Avalonia.OpenGL.Egl -{ - public class EglPlatformOpenGlInterface : IPlatformOpenGlInterface - { - public EglDisplay Display { get; private set; } - public bool CanCreateContexts => true; - public bool CanShareContexts => Display.SupportsSharing; - - public EglContext PrimaryEglContext { get; } - public IGlContext PrimaryContext => PrimaryEglContext; - IPlatformGpuContext IPlatformGpu.PrimaryContext => PrimaryContext; - - public EglPlatformOpenGlInterface(EglDisplay display) - { - Display = display; - PrimaryEglContext = display.CreateContext(null); - } - - public static void TryInitialize() - { - var feature = TryCreate(); - if (feature != null) - AvaloniaLocator.CurrentMutable.Bind().ToConstant(feature); - } - - public static EglPlatformOpenGlInterface TryCreate() => TryCreate(() => new EglDisplay()); - public static EglPlatformOpenGlInterface TryCreate(Func displayFactory) - { - try - { - return new EglPlatformOpenGlInterface(displayFactory()); - } - catch(Exception e) - { - Logger.TryGet(LogEventLevel.Error, "OpenGL")?.Log(null, "Unable to initialize EGL-based rendering: {0}", e); - return null; - } - } - - public IGlContext CreateContext() => Display.CreateContext(null); - public IGlContext CreateSharedContext() => Display.CreateContext(PrimaryEglContext); - - - public EglSurface CreateWindowSurface(IntPtr window) - { - if (window == IntPtr.Zero) - throw new OpenGlException($"Window {window} is invalid."); - - using (PrimaryContext.MakeCurrent()) - { - var s = Display.EglInterface.CreateWindowSurface(Display.Handle, Display.Config, window, - new[] { EGL_NONE, EGL_NONE }); - if (s == IntPtr.Zero) - throw OpenGlException.GetFormattedException("eglCreateWindowSurface", Display.EglInterface); - return new EglSurface(Display, PrimaryEglContext, s); - } - } - - public EglSurface CreatePBufferFromClientBuffer (int bufferType, IntPtr handle, int[] attribs) - { - using (PrimaryContext.MakeCurrent()) - { - var s = Display.EglInterface.CreatePbufferFromClientBuffer(Display.Handle, bufferType, handle, - Display.Config, attribs); - - if (s == IntPtr.Zero) - throw OpenGlException.GetFormattedException("eglCreatePbufferFromClientBuffer", Display.EglInterface); - return new EglSurface(Display, PrimaryEglContext, s); - } - } - } -} diff --git a/src/Avalonia.OpenGL/Egl/EglSurface.cs b/src/Avalonia.OpenGL/Egl/EglSurface.cs index a93751ca9e..f3ee85b3f6 100644 --- a/src/Avalonia.OpenGL/Egl/EglSurface.cs +++ b/src/Avalonia.OpenGL/Egl/EglSurface.cs @@ -6,19 +6,17 @@ namespace Avalonia.OpenGL.Egl public class EglSurface : SafeHandle { private readonly EglDisplay _display; - private readonly EglContext _context; private readonly EglInterface _egl; - public EglSurface(EglDisplay display, EglContext context, IntPtr surface) : base(surface, true) + public EglSurface(EglDisplay display, IntPtr surface) : base(surface, true) { _display = display; - _context = context; _egl = display.EglInterface; } protected override bool ReleaseHandle() { - using (_context.MakeCurrent()) + using (_display.Lock()) _egl.DestroySurface(_display.Handle, handle); return true; } diff --git a/src/Avalonia.OpenGL/GlVersion.cs b/src/Avalonia.OpenGL/GlVersion.cs index 042ff4c2f0..16ed18e45a 100644 --- a/src/Avalonia.OpenGL/GlVersion.cs +++ b/src/Avalonia.OpenGL/GlVersion.cs @@ -6,7 +6,7 @@ namespace Avalonia.OpenGL OpenGLES } - public struct GlVersion + public record struct GlVersion { public GlProfileType Type { get; } public int Major { get; } diff --git a/src/Avalonia.OpenGL/IGlContext.cs b/src/Avalonia.OpenGL/IGlContext.cs index a52a6535da..d45ea931d0 100644 --- a/src/Avalonia.OpenGL/IGlContext.cs +++ b/src/Avalonia.OpenGL/IGlContext.cs @@ -1,9 +1,11 @@ using System; +using System.Collections.Generic; +using Avalonia.OpenGL.Surfaces; using Avalonia.Platform; namespace Avalonia.OpenGL { - public interface IGlContext : IPlatformGpuContext + public interface IGlContext : IPlatformGraphicsContext { GlVersion Version { get; } GlInterface GlInterface { get; } @@ -12,5 +14,13 @@ namespace Avalonia.OpenGL IDisposable MakeCurrent(); IDisposable EnsureCurrent(); bool IsSharedWith(IGlContext context); + bool CanCreateSharedContext { get; } + IGlContext CreateSharedContext(IEnumerable preferredVersions = null); + } + + public interface IGlPlatformSurfaceRenderTargetFactory + { + bool CanRenderToSurface(IGlContext context, object surface); + IGlPlatformSurfaceRenderTarget CreateRenderTarget(IGlContext context, object surface); } } diff --git a/src/Avalonia.OpenGL/IOpenGlAwarePlatformRenderInterface.cs b/src/Avalonia.OpenGL/IOpenGlAwarePlatformRenderInterface.cs deleted file mode 100644 index fdb9162164..0000000000 --- a/src/Avalonia.OpenGL/IOpenGlAwarePlatformRenderInterface.cs +++ /dev/null @@ -1,9 +0,0 @@ -using Avalonia.OpenGL.Imaging; - -namespace Avalonia.OpenGL -{ - public interface IOpenGlAwarePlatformRenderInterface - { - IOpenGlBitmapImpl CreateOpenGlBitmap(PixelSize size, Vector dpi); - } -} diff --git a/src/Avalonia.OpenGL/IOpenGlTextureSharingRenderInterfaceContextFeature.cs b/src/Avalonia.OpenGL/IOpenGlTextureSharingRenderInterfaceContextFeature.cs new file mode 100644 index 0000000000..9c22d446ef --- /dev/null +++ b/src/Avalonia.OpenGL/IOpenGlTextureSharingRenderInterfaceContextFeature.cs @@ -0,0 +1,13 @@ +using System.Collections.Generic; +using Avalonia.OpenGL.Imaging; +using Avalonia.Platform; + +namespace Avalonia.OpenGL +{ + public interface IOpenGlTextureSharingRenderInterfaceContextFeature + { + bool CanCreateSharedContext { get; } + IGlContext CreateSharedContext(IEnumerable preferredVersions = null); + IOpenGlBitmapImpl CreateOpenGlBitmap(PixelSize size, Vector dpi); + } +} diff --git a/src/Avalonia.OpenGL/IPlatformOpenGlInterface.cs b/src/Avalonia.OpenGL/IPlatformOpenGlInterface.cs deleted file mode 100644 index 4ff7997b03..0000000000 --- a/src/Avalonia.OpenGL/IPlatformOpenGlInterface.cs +++ /dev/null @@ -1,15 +0,0 @@ -using Avalonia.Platform; - -namespace Avalonia.OpenGL -{ - public interface IPlatformOpenGlInterface : IPlatformGpu - { - new IGlContext PrimaryContext { get; } - IGlContext CreateSharedContext(); - bool CanShareContexts { get; } - bool CanCreateContexts { get; } - IGlContext CreateContext(); - /*IGlContext TryCreateContext(GlVersion version); - */ - } -} diff --git a/src/Avalonia.OpenGL/Imaging/OpenGlBitmap.cs b/src/Avalonia.OpenGL/Imaging/OpenGlBitmap.cs index 7af44cd624..53013ae5a3 100644 --- a/src/Avalonia.OpenGL/Imaging/OpenGlBitmap.cs +++ b/src/Avalonia.OpenGL/Imaging/OpenGlBitmap.cs @@ -10,19 +10,15 @@ namespace Avalonia.OpenGL.Imaging { private IOpenGlBitmapImpl _impl; - public OpenGlBitmap(PixelSize size, Vector dpi) - : base(CreateOrThrow(size, dpi)) + public OpenGlBitmap(IOpenGlTextureSharingRenderInterfaceContextFeature feature, + PixelSize size, Vector dpi) + : base(CreateOrThrow(feature, size, dpi)) { _impl = (IOpenGlBitmapImpl)PlatformImpl.Item; } - - static IOpenGlBitmapImpl CreateOrThrow(PixelSize size, Vector dpi) - { - if (!(AvaloniaLocator.Current.GetService() is IOpenGlAwarePlatformRenderInterface - glAware)) - throw new PlatformNotSupportedException("Rendering platform does not support OpenGL integration"); - return glAware.CreateOpenGlBitmap(size, dpi); - } + + static IOpenGlBitmapImpl CreateOrThrow(IOpenGlTextureSharingRenderInterfaceContextFeature feature, + PixelSize size, Vector dpi) => feature.CreateOpenGlBitmap(size, dpi); public IOpenGlBitmapAttachment CreateFramebufferAttachment(IGlContext context) => _impl.CreateFramebufferAttachment(context, SetIsDirty); diff --git a/src/Avalonia.OpenGL/OpenGlException.cs b/src/Avalonia.OpenGL/OpenGlException.cs index 196f507ad8..efe305dba5 100644 --- a/src/Avalonia.OpenGL/OpenGlException.cs +++ b/src/Avalonia.OpenGL/OpenGlException.cs @@ -18,25 +18,33 @@ namespace Avalonia.OpenGL public static OpenGlException GetFormattedException(string funcName, EglInterface egl) { - return GetFormattedException(typeof(EglErrors), funcName, egl.GetError()); + return GetFormattedEglException(funcName, egl.GetError()); } public static OpenGlException GetFormattedException(string funcName, GlInterface gl) { - return GetFormattedException(typeof(GlErrors), funcName, gl.GetError()); + var err = gl.GetError(); + return GetFormattedException(funcName, (GlErrors)err, err); } - private static OpenGlException GetFormattedException(Type consts, string funcName, int errorCode) + public static OpenGlException GetFormattedEglException(string funcName, int errorCode) => + GetFormattedException(funcName, (EglErrors)errorCode,errorCode); + + private static OpenGlException GetFormattedException(string funcName, T errorCode, int intErrorCode) where T : struct, Enum { try { - string errorName = Enum.GetName(consts, errorCode); +#if NET6_0_OR_GREATER + var errorName = Enum.GetName(errorCode); +#else + var errorName = Enum.GetName(typeof(T), errorCode); +#endif return new OpenGlException( - $"{funcName} failed with error {errorName} (0x{errorCode.ToString("X")})", errorCode); + $"{funcName} failed with error {errorName} (0x{errorCode.ToString("X")})", intErrorCode); } catch (ArgumentException) { - return new OpenGlException($"{funcName} failed with error 0x{errorCode.ToString("X")}", errorCode); + return new OpenGlException($"{funcName} failed with error 0x{errorCode.ToString("X")}", intErrorCode); } } } diff --git a/src/Avalonia.OpenGL/Surfaces/IGlPlatformSurface.cs b/src/Avalonia.OpenGL/Surfaces/IGlPlatformSurface.cs index 875c215336..d80e72e4e7 100644 --- a/src/Avalonia.OpenGL/Surfaces/IGlPlatformSurface.cs +++ b/src/Avalonia.OpenGL/Surfaces/IGlPlatformSurface.cs @@ -2,6 +2,6 @@ namespace Avalonia.OpenGL.Surfaces { public interface IGlPlatformSurface { - IGlPlatformSurfaceRenderTarget CreateGlRenderTarget(); + IGlPlatformSurfaceRenderTarget CreateGlRenderTarget(IGlContext context); } } diff --git a/src/Avalonia.Remote.Protocol/MetsysBson.cs b/src/Avalonia.Remote.Protocol/MetsysBson.cs index d011a963be..c0263b3518 100644 --- a/src/Avalonia.Remote.Protocol/MetsysBson.cs +++ b/src/Avalonia.Remote.Protocol/MetsysBson.cs @@ -1370,7 +1370,7 @@ namespace Metsys.Bson var pattern = ReadName(); var optionsString = ReadName(); - var options = RegexOptions.None; + var options = RegexOptions.Compiled; if (optionsString.Contains('e')) options = options | RegexOptions.ECMAScript; if (optionsString.Contains('i')) options = options | RegexOptions.IgnoreCase; if (optionsString.Contains('l')) options = options | RegexOptions.CultureInvariant; diff --git a/src/Avalonia.Themes.Fluent/Accents/Base.xaml b/src/Avalonia.Themes.Fluent/Accents/Base.xaml index 259d107b5c..479bcd8531 100644 --- a/src/Avalonia.Themes.Fluent/Accents/Base.xaml +++ b/src/Avalonia.Themes.Fluent/Accents/Base.xaml @@ -22,6 +22,7 @@ 10,6,6,5 20 20 + 8,5,8,6 3 diff --git a/src/Avalonia.Themes.Fluent/Accents/BaseDark.xaml b/src/Avalonia.Themes.Fluent/Accents/BaseDark.xaml index 89841c92c0..0192fb1b54 100644 --- a/src/Avalonia.Themes.Fluent/Accents/BaseDark.xaml +++ b/src/Avalonia.Themes.Fluent/Accents/BaseDark.xaml @@ -36,6 +36,7 @@ + diff --git a/src/Avalonia.Themes.Fluent/Accents/BaseLight.xaml b/src/Avalonia.Themes.Fluent/Accents/BaseLight.xaml index 89b646fb52..a9e5ed949a 100644 --- a/src/Avalonia.Themes.Fluent/Accents/BaseLight.xaml +++ b/src/Avalonia.Themes.Fluent/Accents/BaseLight.xaml @@ -36,6 +36,7 @@ + diff --git a/src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesDark.xaml b/src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesDark.xaml index 71a8bc3a3c..810065fc9b 100644 --- a/src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesDark.xaml +++ b/src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesDark.xaml @@ -636,4 +636,8 @@ + + + + diff --git a/src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesLight.xaml b/src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesLight.xaml index d764e1616c..bccc47b9b8 100644 --- a/src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesLight.xaml +++ b/src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesLight.xaml @@ -631,4 +631,8 @@ - + + + + + \ No newline at end of file diff --git a/src/Avalonia.Themes.Fluent/Controls/Button.xaml b/src/Avalonia.Themes.Fluent/Controls/Button.xaml index 7828fd52ed..126f2c22e0 100644 --- a/src/Avalonia.Themes.Fluent/Controls/Button.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/Button.xaml @@ -8,8 +8,6 @@ - - 8,5,8,6 diff --git a/src/Avalonia.Themes.Fluent/Controls/EmbeddableControlRoot.xaml b/src/Avalonia.Themes.Fluent/Controls/EmbeddableControlRoot.xaml index f8b4854553..67e70d154f 100644 --- a/src/Avalonia.Themes.Fluent/Controls/EmbeddableControlRoot.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/EmbeddableControlRoot.xaml @@ -1,6 +1,7 @@ + diff --git a/src/Avalonia.Themes.Fluent/Controls/FluentControls.xaml b/src/Avalonia.Themes.Fluent/Controls/FluentControls.xaml index a029be6b8d..2c3550a72f 100644 --- a/src/Avalonia.Themes.Fluent/Controls/FluentControls.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/FluentControls.xaml @@ -4,70 +4,71 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Avalonia.Themes.Fluent/Controls/ManagedFileChooser.xaml b/src/Avalonia.Themes.Fluent/Controls/ManagedFileChooser.xaml index d1707d0af2..63b3d46a41 100644 --- a/src/Avalonia.Themes.Fluent/Controls/ManagedFileChooser.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/ManagedFileChooser.xaml @@ -153,9 +153,13 @@ - + @@ -322,7 +326,6 @@ + + + + + + diff --git a/src/Avalonia.Themes.Fluent/Controls/RepeatButton.xaml b/src/Avalonia.Themes.Fluent/Controls/RepeatButton.xaml index a54187104b..fd04c85fed 100644 --- a/src/Avalonia.Themes.Fluent/Controls/RepeatButton.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/RepeatButton.xaml @@ -10,8 +10,6 @@ - 8,5,8,6 - diff --git a/src/Avalonia.Themes.Fluent/Controls/ToggleButton.xaml b/src/Avalonia.Themes.Fluent/Controls/ToggleButton.xaml index 7a46f21534..da2021790a 100644 --- a/src/Avalonia.Themes.Fluent/Controls/ToggleButton.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/ToggleButton.xaml @@ -10,8 +10,6 @@ - 8,5,8,6 - diff --git a/src/Avalonia.Themes.Fluent/Controls/TreeViewItem.xaml b/src/Avalonia.Themes.Fluent/Controls/TreeViewItem.xaml index 9171791a0f..0bed388ca4 100644 --- a/src/Avalonia.Themes.Fluent/Controls/TreeViewItem.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/TreeViewItem.xaml @@ -75,6 +75,7 @@ MinHeight="{TemplateBinding MinHeight}" TemplatedControl.IsTemplateFocusTarget="True"> - - + + diff --git a/src/Avalonia.Themes.Fluent/FluentTheme.xaml.cs b/src/Avalonia.Themes.Fluent/FluentTheme.xaml.cs index eea3d3ad08..a8297953a8 100644 --- a/src/Avalonia.Themes.Fluent/FluentTheme.xaml.cs +++ b/src/Avalonia.Themes.Fluent/FluentTheme.xaml.cs @@ -99,15 +99,15 @@ namespace Avalonia.Themes.Fluent var themeVariantResource1 = Mode == FluentThemeMode.Dark ? _baseDark : _baseLight; var themeVariantResource2 = Mode == FluentThemeMode.Dark ? _fluentDark : _fluentLight; var dict = Resources.MergedDictionaries; - if (dict.Count == 2) + if (dict.Count == 0) { - dict.Insert(1, themeVariantResource1); + dict.Add(themeVariantResource1); dict.Add(themeVariantResource2); } else { - dict[1] = themeVariantResource1; - dict[3] = themeVariantResource2; + dict[0] = themeVariantResource1; + dict[1] = themeVariantResource2; } } diff --git a/src/Avalonia.Themes.Simple/Accents/BaseDark.xaml b/src/Avalonia.Themes.Simple/Accents/BaseDark.xaml index 9ad9f70c98..88c2681f65 100644 --- a/src/Avalonia.Themes.Simple/Accents/BaseDark.xaml +++ b/src/Avalonia.Themes.Simple/Accents/BaseDark.xaml @@ -31,5 +31,8 @@ + + + diff --git a/src/Avalonia.Themes.Simple/Accents/BaseLight.xaml b/src/Avalonia.Themes.Simple/Accents/BaseLight.xaml index f96425cf06..77166a9d8a 100644 --- a/src/Avalonia.Themes.Simple/Accents/BaseLight.xaml +++ b/src/Avalonia.Themes.Simple/Accents/BaseLight.xaml @@ -31,5 +31,7 @@ - + + + diff --git a/src/Avalonia.Themes.Simple/Controls/EmbeddableControlRoot.xaml b/src/Avalonia.Themes.Simple/Controls/EmbeddableControlRoot.xaml index 79d6c6d917..3d52464858 100644 --- a/src/Avalonia.Themes.Simple/Controls/EmbeddableControlRoot.xaml +++ b/src/Avalonia.Themes.Simple/Controls/EmbeddableControlRoot.xaml @@ -2,6 +2,7 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"> + diff --git a/src/Avalonia.Themes.Simple/Controls/ManagedFileChooser.xaml b/src/Avalonia.Themes.Simple/Controls/ManagedFileChooser.xaml index 61dae9b445..5a1fa4ec6d 100644 --- a/src/Avalonia.Themes.Simple/Controls/ManagedFileChooser.xaml +++ b/src/Avalonia.Themes.Simple/Controls/ManagedFileChooser.xaml @@ -11,6 +11,12 @@ + + + + @@ -54,6 +60,14 @@ + + + diff --git a/src/Avalonia.Themes.Simple/Controls/RefreshContainer.xaml b/src/Avalonia.Themes.Simple/Controls/RefreshContainer.xaml new file mode 100644 index 0000000000..04b2a1bf29 --- /dev/null +++ b/src/Avalonia.Themes.Simple/Controls/RefreshContainer.xaml @@ -0,0 +1,24 @@ + + + + + + + + + + + + + diff --git a/src/Avalonia.Themes.Simple/Controls/RefreshVisualizer.xaml b/src/Avalonia.Themes.Simple/Controls/RefreshVisualizer.xaml new file mode 100644 index 0000000000..bd7e43530a --- /dev/null +++ b/src/Avalonia.Themes.Simple/Controls/RefreshVisualizer.xaml @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + diff --git a/src/Avalonia.Themes.Simple/Controls/SimpleControls.xaml b/src/Avalonia.Themes.Simple/Controls/SimpleControls.xaml index 4aefa0136c..093adaeab2 100644 --- a/src/Avalonia.Themes.Simple/Controls/SimpleControls.xaml +++ b/src/Avalonia.Themes.Simple/Controls/SimpleControls.xaml @@ -3,68 +3,70 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Avalonia.Themes.Simple/Controls/TreeViewItem.xaml b/src/Avalonia.Themes.Simple/Controls/TreeViewItem.xaml index 6ca3262419..75da4bc579 100644 --- a/src/Avalonia.Themes.Simple/Controls/TreeViewItem.xaml +++ b/src/Avalonia.Themes.Simple/Controls/TreeViewItem.xaml @@ -44,6 +44,7 @@ Focusable="True" TemplatedControl.IsTemplateFocusTarget="True"> - + diff --git a/src/Avalonia.Themes.Simple/SimpleTheme.xaml.cs b/src/Avalonia.Themes.Simple/SimpleTheme.xaml.cs index 56c4cbeac6..42dfafd7e0 100644 --- a/src/Avalonia.Themes.Simple/SimpleTheme.xaml.cs +++ b/src/Avalonia.Themes.Simple/SimpleTheme.xaml.cs @@ -58,13 +58,13 @@ namespace Avalonia.Themes.Simple { var themeVariantResource = Mode == SimpleThemeMode.Dark ? _simpleDark : _simpleLight; var dict = Resources.MergedDictionaries; - if (dict.Count == 1) + if (dict.Count == 0) { dict.Add(themeVariantResource); } else { - dict[1] = themeVariantResource; + dict[0] = themeVariantResource; } } } diff --git a/src/Avalonia.X11/Glx/GlxContext.cs b/src/Avalonia.X11/Glx/GlxContext.cs index e9cb88cb8f..def5228e94 100644 --- a/src/Avalonia.X11/Glx/GlxContext.cs +++ b/src/Avalonia.X11/Glx/GlxContext.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Reactive.Disposables; using System.Threading; using Avalonia.OpenGL; @@ -70,6 +71,8 @@ namespace Avalonia.X11.Glx } public IDisposable MakeCurrent() => MakeCurrent(_defaultXid); + public bool IsLost => false; + public IDisposable EnsureCurrent() { if(IsCurrent) @@ -86,6 +89,11 @@ namespace Avalonia.X11.Glx || _sharedWith != null && _sharedWith == c._sharedWith; } + public bool CanCreateSharedContext => true; + + public IGlContext CreateSharedContext(IEnumerable preferredVersions = null) => + Display.CreateContext(_sharedWith ?? this); + public IDisposable MakeCurrent(IntPtr xid) { Monitor.Enter(_lock); @@ -114,5 +122,7 @@ namespace Avalonia.X11.Glx if (_ownsPBuffer) Glx.DestroyPbuffer(_x11.Display, _defaultXid); } + + public object TryGetFeature(Type featureType) => null; } } diff --git a/src/Avalonia.X11/Glx/GlxDisplay.cs b/src/Avalonia.X11/Glx/GlxDisplay.cs index 1e70608168..9b8a8f0b5b 100644 --- a/src/Avalonia.X11/Glx/GlxDisplay.cs +++ b/src/Avalonia.X11/Glx/GlxDisplay.cs @@ -111,8 +111,8 @@ namespace Avalonia.X11.Glx return Glx.CreatePbuffer(_x11.Display, _fbconfig, new[] { GLX_PBUFFER_WIDTH, 1, GLX_PBUFFER_HEIGHT, 1, 0 }); } - - public GlxContext CreateContext() => CreateContext(); + public GlxContext CreateContext() => CreateContext(CreatePBuffer(), null, DeferredContext.SampleCount, + DeferredContext.StencilSize, true); public GlxContext CreateContext(IGlContext share) => CreateContext(CreatePBuffer(), share, share.SampleCount, share.StencilSize, true); diff --git a/src/Avalonia.X11/Glx/GlxGlPlatformSurface.cs b/src/Avalonia.X11/Glx/GlxGlPlatformSurface.cs index cb4ab4aca0..ebb1e18723 100644 --- a/src/Avalonia.X11/Glx/GlxGlPlatformSurface.cs +++ b/src/Avalonia.X11/Glx/GlxGlPlatformSurface.cs @@ -9,20 +9,16 @@ namespace Avalonia.X11.Glx class GlxGlPlatformSurface: IGlPlatformSurface { - private readonly GlxDisplay _display; - private readonly GlxContext _context; private readonly EglGlPlatformSurface.IEglWindowGlPlatformSurfaceInfo _info; - public GlxGlPlatformSurface(GlxDisplay display, GlxContext context, EglGlPlatformSurface.IEglWindowGlPlatformSurfaceInfo info) + public GlxGlPlatformSurface(EglGlPlatformSurface.IEglWindowGlPlatformSurfaceInfo info) { - _display = display; - _context = context; _info = info; } - public IGlPlatformSurfaceRenderTarget CreateGlRenderTarget() + public IGlPlatformSurfaceRenderTarget CreateGlRenderTarget(IGlContext context) { - return new RenderTarget(_context, _info); + return new RenderTarget((GlxContext)context, _info); } class RenderTarget : IGlPlatformSurfaceRenderTarget diff --git a/src/Avalonia.X11/Glx/GlxPlatformFeature.cs b/src/Avalonia.X11/Glx/GlxPlatformFeature.cs index 0968adc799..bc50fa61aa 100644 --- a/src/Avalonia.X11/Glx/GlxPlatformFeature.cs +++ b/src/Avalonia.X11/Glx/GlxPlatformFeature.cs @@ -6,38 +6,36 @@ using Avalonia.Platform; namespace Avalonia.X11.Glx { - class GlxPlatformOpenGlInterface : IPlatformOpenGlInterface + class GlxPlatformGraphics : IPlatformGraphics { public GlxDisplay Display { get; private set; } public bool CanCreateContexts => true; public bool CanShareContexts => true; - public IGlContext CreateContext() => Display.CreateContext(); - public IGlContext CreateSharedContext() => Display.CreateContext(PrimaryContext); - public GlxContext DeferredContext { get; private set; } - public IGlContext PrimaryContext => DeferredContext; - IPlatformGpuContext IPlatformGpu.PrimaryContext => PrimaryContext; + public bool UsesSharedContext => false; + IPlatformGraphicsContext IPlatformGraphics.CreateContext() => Display.CreateContext(); + + public IPlatformGraphicsContext GetSharedContext() => throw new NotSupportedException(); public static bool TryInitialize(X11Info x11, IList glProfiles) { var feature = TryCreate(x11, glProfiles); if (feature != null) { - AvaloniaLocator.CurrentMutable.Bind().ToConstant(feature); + AvaloniaLocator.CurrentMutable.Bind().ToConstant(feature); return true; } return false; } - public static GlxPlatformOpenGlInterface TryCreate(X11Info x11, IList glProfiles) + public static GlxPlatformGraphics TryCreate(X11Info x11, IList glProfiles) { try { var disp = new GlxDisplay(x11, glProfiles); - return new GlxPlatformOpenGlInterface + return new GlxPlatformGraphics { - Display = disp, - DeferredContext = disp.DeferredContext + Display = disp }; } catch(Exception e) diff --git a/src/Avalonia.X11/X11Clipboard.cs b/src/Avalonia.X11/X11Clipboard.cs index d428d82744..2aa7797067 100644 --- a/src/Avalonia.X11/X11Clipboard.cs +++ b/src/Avalonia.X11/X11Clipboard.cs @@ -18,8 +18,6 @@ namespace Avalonia.X11 private TaskCompletionSource _requestedDataTcs; private readonly IntPtr[] _textAtoms; private readonly IntPtr _avaloniaSaveTargetsAtom; - private readonly Dictionary _formatAtoms = new Dictionary(); - private readonly Dictionary _atomFormats = new Dictionary(); public X11Clipboard(AvaloniaX11Platform platform) { diff --git a/src/Avalonia.X11/X11CursorFactory.cs b/src/Avalonia.X11/X11CursorFactory.cs index 6041b53a62..16de10e163 100644 --- a/src/Avalonia.X11/X11CursorFactory.cs +++ b/src/Avalonia.X11/X11CursorFactory.cs @@ -113,7 +113,8 @@ namespace Avalonia.X11 image->yhot = hotSpot.Y; image->pixels = (IntPtr)(image + 1); - using (var renderTarget = platformRenderInterface.CreateRenderTarget(new[] { this })) + using (var cpuContext = platformRenderInterface.CreateBackendContext(null)) + using (var renderTarget = cpuContext.CreateRenderTarget(new[] { this })) using (var ctx = renderTarget.CreateDrawingContext(null)) { var r = new Rect(_pixelSize.ToSize(1)); diff --git a/src/Avalonia.X11/X11IconLoader.cs b/src/Avalonia.X11/X11IconLoader.cs index 4ae1c1599f..bf59d72c0f 100644 --- a/src/Avalonia.X11/X11IconLoader.cs +++ b/src/Avalonia.X11/X11IconLoader.cs @@ -41,7 +41,8 @@ namespace Avalonia.X11 _width = Math.Min(bitmap.PixelSize.Width, 128); _height = Math.Min(bitmap.PixelSize.Height, 128); _bdata = new uint[_width * _height]; - using(var rt = AvaloniaLocator.Current.GetService().CreateRenderTarget(new[]{this})) + using(var cpuContext = AvaloniaLocator.Current.GetRequiredService().CreateBackendContext(null)) + using(var rt = cpuContext.CreateRenderTarget(new[]{this})) using (var ctx = rt.CreateDrawingContext(null)) ctx.DrawBitmap(bitmap.PlatformImpl, 1, new Rect(bitmap.Size), new Rect(0, 0, _width, _height)); diff --git a/src/Avalonia.X11/X11ImmediateRendererProxy.cs b/src/Avalonia.X11/X11ImmediateRendererProxy.cs index 293e5110f7..ef061cbe5c 100644 --- a/src/Avalonia.X11/X11ImmediateRendererProxy.cs +++ b/src/Avalonia.X11/X11ImmediateRendererProxy.cs @@ -1,5 +1,7 @@ using System; using System.Collections.Generic; +using System.Threading.Tasks; +using Avalonia.Platform; using Avalonia.Rendering; using Avalonia.Threading; using Avalonia.VisualTree; @@ -14,11 +16,11 @@ namespace Avalonia.X11 private bool _running; private object _lock = new object(); - public X11ImmediateRendererProxy(Visual root, IRenderLoop loop) + public X11ImmediateRendererProxy(Visual root, IRenderLoop loop, Func renderTargetFactory, + PlatformRenderInterfaceContextManager renderContext) { _loop = loop; - _renderer = new ImmediateRenderer(root); - + _renderer = new ImmediateRenderer(root, renderTargetFactory, renderContext); } public void Dispose() @@ -92,6 +94,9 @@ namespace Avalonia.X11 _renderer.Stop(); } + public ValueTask TryGetRenderInterfaceFeature(Type featureType) => + _renderer.TryGetRenderInterfaceFeature(featureType); + public bool NeedsUpdate => false; public void Update(TimeSpan time) { diff --git a/src/Avalonia.X11/X11Platform.cs b/src/Avalonia.X11/X11Platform.cs index cbb782edd0..e44b5ded14 100644 --- a/src/Avalonia.X11/X11Platform.cs +++ b/src/Avalonia.X11/X11Platform.cs @@ -30,6 +30,7 @@ namespace Avalonia.X11 public X11Info Info { get; private set; } public IX11Screens X11Screens { get; private set; } public Compositor Compositor { get; private set; } + public PlatformRenderInterfaceContextManager RenderInterface { get; private set; } public IScreenImpl Screens { get; private set; } public X11PlatformOptions Options { get; private set; } public IntPtr OrphanedWindow { get; private set; } @@ -96,17 +97,17 @@ namespace Avalonia.X11 if (options.UseGpu) { if (options.UseEGL) - EglPlatformOpenGlInterface.TryInitialize(); + EglPlatformGraphics.TryInitialize(); else - GlxPlatformOpenGlInterface.TryInitialize(Info, Options.GlProfiles); + GlxPlatformGraphics.TryInitialize(Info, Options.GlProfiles); } - var gl = AvaloniaLocator.Current.GetService(); - if (gl != null) - AvaloniaLocator.CurrentMutable.Bind().ToConstant(gl); - + var gl = AvaloniaLocator.Current.GetService(); + if (options.UseCompositor) Compositor = new Compositor(AvaloniaLocator.Current.GetService()!, gl); + else + RenderInterface = new(gl); } diff --git a/src/Avalonia.X11/X11Structs.cs b/src/Avalonia.X11/X11Structs.cs index 7e46606c36..26515762b4 100644 --- a/src/Avalonia.X11/X11Structs.cs +++ b/src/Avalonia.X11/X11Structs.cs @@ -1768,7 +1768,9 @@ namespace Avalonia.X11 { } [StructLayout(LayoutKind.Sequential)] +#pragma warning disable CA1815 // Override equals and operator equals on value types public unsafe struct XImage +#pragma warning restore CA1815 // Override equals and operator equals on value types { public int width, height; /* size of image */ public int xoffset; /* number of pixels offset in X direction */ @@ -1799,7 +1801,7 @@ namespace Avalonia.X11 { internal IntPtr green_mask; internal IntPtr blue_mask; internal int colormap_size; - internal int bits_per_rgb; + internal int bits_per_rgb; } internal enum XIMFeedback diff --git a/src/Avalonia.X11/X11Window.cs b/src/Avalonia.X11/X11Window.cs index 2f84e15b32..160401c5fc 100644 --- a/src/Avalonia.X11/X11Window.cs +++ b/src/Avalonia.X11/X11Window.cs @@ -74,7 +74,7 @@ namespace Avalonia.X11 _touch = new TouchDevice(); _keyboard = platform.KeyboardDevice; - var glfeature = AvaloniaLocator.Current.GetService(); + var glfeature = AvaloniaLocator.Current.GetService(); XSetWindowAttributes attr = new XSetWindowAttributes(); var valueMask = default(SetWindowValuemask); @@ -96,13 +96,13 @@ namespace Avalonia.X11 // OpenGL seems to be do weird things to it's current window which breaks resize sometimes _useRenderWindow = glfeature != null; - var glx = glfeature as GlxPlatformOpenGlInterface; + var glx = glfeature as GlxPlatformGraphics; if (glx != null) visualInfo = *glx.Display.VisualInfo; else if (glfeature == null) visualInfo = _x11.TransparentVisualInfo; - var egl = glfeature as EglPlatformOpenGlInterface; + var egl = glfeature as EglPlatformGraphics; var visual = IntPtr.Zero; var depth = 24; @@ -176,11 +176,9 @@ namespace Avalonia.X11 if (egl != null) surfaces.Insert(0, - new EglGlPlatformSurface(egl, - new SurfaceInfo(this, _x11.DeferredDisplay, _handle, _renderHandle))); + new EglGlPlatformSurface(new SurfaceInfo(this, _x11.DeferredDisplay, _handle, _renderHandle))); if (glx != null) - surfaces.Insert(0, new GlxGlPlatformSurface(glx.Display, glx.DeferredContext, - new SurfaceInfo(this, _x11.Display, _handle, _renderHandle))); + surfaces.Insert(0, new GlxGlPlatformSurface(new SurfaceInfo(this, _x11.Display, _handle, _renderHandle))); surfaces.Add(Handle); @@ -391,12 +389,14 @@ namespace Avalonia.X11 return _platform.Options.UseDeferredRendering ? _platform.Options.UseCompositor - ? new CompositingRenderer(root, this._platform.Compositor) - : new DeferredRenderer(root, loop) + ? new CompositingRenderer(root, this._platform.Compositor, () => Surfaces) + : new DeferredRenderer(root, loop, () => _platform.RenderInterface.CreateRenderTarget(Surfaces), _platform.RenderInterface) { RenderOnlyOnRenderThread = true } - : (IRenderer)new X11ImmediateRendererProxy((Visual)root, loop); + : new X11ImmediateRendererProxy((Visual)root, loop, + () => _platform.RenderInterface.CreateRenderTarget(Surfaces), + _platform.RenderInterface); } void OnEvent(ref XEvent ev) diff --git a/src/Browser/Avalonia.Browser/AvaloniaView.cs b/src/Browser/Avalonia.Browser/AvaloniaView.cs index a407e1e4d8..abda618b0d 100644 --- a/src/Browser/Avalonia.Browser/AvaloniaView.cs +++ b/src/Browser/Avalonia.Browser/AvaloniaView.cs @@ -11,6 +11,7 @@ using Avalonia.Controls.Platform; using Avalonia.Input; using Avalonia.Input.Raw; using Avalonia.Input.TextInput; +using Avalonia.Platform; using Avalonia.Rendering.Composition; using Avalonia.Threading; using SkiaSharp; @@ -106,7 +107,7 @@ namespace Avalonia.Browser _dpi = DomHelper.ObserveDpi(OnDpiChanged); - _useGL = skiaOptions?.CustomGpuFactory != null; + _useGL = AvaloniaLocator.Current.GetRequiredService() != null; if (_useGL) { diff --git a/src/Browser/Avalonia.Browser/BrowserNativeControlHost.cs b/src/Browser/Avalonia.Browser/BrowserNativeControlHost.cs index 7e91d29019..c2e54c7ed7 100644 --- a/src/Browser/Avalonia.Browser/BrowserNativeControlHost.cs +++ b/src/Browser/Avalonia.Browser/BrowserNativeControlHost.cs @@ -55,12 +55,6 @@ namespace Avalonia.Browser private class Attachment : INativeControlHostControlTopLevelAttachment { - private const string InitializeWithChildHandleSymbol = "InitializeWithChildHandle"; - private const string AttachToSymbol = "AttachTo"; - private const string ShowInBoundsSymbol = "ShowInBounds"; - private const string HideWithSizeSymbol = "HideWithSize"; - private const string ReleaseChildSymbol = "ReleaseChild"; - private JSObject? _native; private BrowserNativeControlHost? _attachedTo; diff --git a/src/Browser/Avalonia.Browser/BrowserSingleViewLifetime.cs b/src/Browser/Avalonia.Browser/BrowserSingleViewLifetime.cs index ee4f6eca9b..b6c766b2a5 100644 --- a/src/Browser/Avalonia.Browser/BrowserSingleViewLifetime.cs +++ b/src/Browser/Avalonia.Browser/BrowserSingleViewLifetime.cs @@ -48,7 +48,6 @@ public static class WebAppBuilder { return builder .UseWindowingSubsystem(BrowserWindowingPlatform.Register) - .UseSkia() - .With(new SkiaOptions { CustomGpuFactory = () => new BrowserSkiaGpu() }); + .UseSkia(); } } diff --git a/src/Browser/Avalonia.Browser/BrowserTopLevelImpl.cs b/src/Browser/Avalonia.Browser/BrowserTopLevelImpl.cs index 69e2d27181..85725e4cf3 100644 --- a/src/Browser/Avalonia.Browser/BrowserTopLevelImpl.cs +++ b/src/Browser/Avalonia.Browser/BrowserTopLevelImpl.cs @@ -166,7 +166,8 @@ namespace Avalonia.Browser public IRenderer CreateRenderer(IRenderRoot root) { var loop = AvaloniaLocator.Current.GetRequiredService(); - return new CompositingRenderer(root, new Compositor(loop, null)); + return new CompositingRenderer(root, + new Compositor(loop, AvaloniaLocator.Current.GetRequiredService()), () => Surfaces); } public void Invalidate(Rect rect) diff --git a/src/Browser/Avalonia.Browser/Skia/BrowserSkiaGpu.cs b/src/Browser/Avalonia.Browser/Skia/BrowserSkiaGpu.cs index a96ead93cb..e56efdb4a8 100644 --- a/src/Browser/Avalonia.Browser/Skia/BrowserSkiaGpu.cs +++ b/src/Browser/Avalonia.Browser/Skia/BrowserSkiaGpu.cs @@ -1,4 +1,7 @@ +using System; using System.Collections.Generic; +using System.Reactive.Disposables; +using Avalonia.Platform; using Avalonia.Skia; namespace Avalonia.Browser.Skia @@ -22,5 +25,28 @@ namespace Avalonia.Browser.Skia { return null; } + + public void Dispose() + { + + } + + public object? TryGetFeature(Type t) => null; + + public bool IsLost => false; + + public IDisposable EnsureCurrent() + { + return Disposable.Empty; + } + } + + class BrowserSkiaGraphics : IPlatformGraphics + { + private BrowserSkiaGpu _skia = new(); + public bool UsesSharedContext => true; + public IPlatformGraphicsContext CreateContext() => throw new NotSupportedException(); + + public IPlatformGraphicsContext GetSharedContext() => _skia; } } diff --git a/src/Browser/Avalonia.Browser/WebEmbeddableControlRoot.cs b/src/Browser/Avalonia.Browser/WebEmbeddableControlRoot.cs index e389ee98ea..df1a24fa0f 100644 --- a/src/Browser/Avalonia.Browser/WebEmbeddableControlRoot.cs +++ b/src/Browser/Avalonia.Browser/WebEmbeddableControlRoot.cs @@ -18,7 +18,7 @@ namespace Avalonia.Browser _onFirstRender = onFirstRender; } - public Rect Bounds => Rect.Empty; + public Rect Bounds => default; public bool HasRendered => _hasRendered; diff --git a/src/Browser/Avalonia.Browser/WindowingPlatform.cs b/src/Browser/Avalonia.Browser/WindowingPlatform.cs index 6535e9534c..6493374a50 100644 --- a/src/Browser/Avalonia.Browser/WindowingPlatform.cs +++ b/src/Browser/Avalonia.Browser/WindowingPlatform.cs @@ -1,5 +1,6 @@ using System; using System.Threading; +using Avalonia.Browser.Skia; using Avalonia.Input; using Avalonia.Input.Platform; using Avalonia.Platform; @@ -43,6 +44,7 @@ namespace Avalonia.Browser .Bind().ToConstant(new RenderLoop()) .Bind().ToConstant(ManualTriggerRenderTimer.Instance) .Bind().ToConstant(instance) + .Bind().ToConstant(new BrowserSkiaGraphics()) .Bind().ToSingleton() .Bind().ToSingleton(); } diff --git a/src/Linux/Avalonia.LinuxFramebuffer/FramebufferToplevelImpl.cs b/src/Linux/Avalonia.LinuxFramebuffer/FramebufferToplevelImpl.cs index b64423ec10..ac54365f51 100644 --- a/src/Linux/Avalonia.LinuxFramebuffer/FramebufferToplevelImpl.cs +++ b/src/Linux/Avalonia.LinuxFramebuffer/FramebufferToplevelImpl.cs @@ -1,4 +1,4 @@ -using System; + using System; using System.Collections.Generic; using Avalonia.Controls; using Avalonia.Input; @@ -33,7 +33,7 @@ namespace Avalonia.LinuxFramebuffer { var factory = AvaloniaLocator.Current.GetService(); var renderLoop = AvaloniaLocator.Current.GetService(); - return factory?.Create(root, renderLoop) ?? new CompositingRenderer(root, LinuxFramebufferPlatform.Compositor); + return factory?.Create(root, renderLoop) ?? new CompositingRenderer(root, LinuxFramebufferPlatform.Compositor, () => Surfaces); } public void Dispose() diff --git a/src/Linux/Avalonia.LinuxFramebuffer/Input/EvDev/EvDevBackend.cs b/src/Linux/Avalonia.LinuxFramebuffer/Input/EvDev/EvDevBackend.cs index 1e3c4bed48..686050e7c2 100644 --- a/src/Linux/Avalonia.LinuxFramebuffer/Input/EvDev/EvDevBackend.cs +++ b/src/Linux/Avalonia.LinuxFramebuffer/Input/EvDev/EvDevBackend.cs @@ -3,7 +3,6 @@ using System.Collections.Generic; using System.Threading; using Avalonia.Input; using Avalonia.Input.Raw; -using Avalonia.Threading; using static Avalonia.LinuxFramebuffer.NativeUnsafeMethods; namespace Avalonia.LinuxFramebuffer.Input.EvDev @@ -13,7 +12,6 @@ namespace Avalonia.LinuxFramebuffer.Input.EvDev private readonly EvDevDeviceDescription[] _deviceDescriptions; private readonly List _handlers = new List(); private int _epoll; - private object _lock = new object(); private Action _onInput; private IInputRoot _inputRoot; private RawEventGroupingThreadingHelper _inputQueue; diff --git a/src/Linux/Avalonia.LinuxFramebuffer/Input/LibInput/LibInputBackend.cs b/src/Linux/Avalonia.LinuxFramebuffer/Input/LibInput/LibInputBackend.cs index 77e8202fac..bff9ddc55c 100644 --- a/src/Linux/Avalonia.LinuxFramebuffer/Input/LibInput/LibInputBackend.cs +++ b/src/Linux/Avalonia.LinuxFramebuffer/Input/LibInput/LibInputBackend.cs @@ -11,7 +11,6 @@ namespace Avalonia.LinuxFramebuffer.Input.LibInput { private IScreenInfoProvider _screen; private IInputRoot _inputRoot; - private readonly Queue _inputThreadActions = new Queue(); private TouchDevice _touch = new TouchDevice(); private const string LibInput = nameof(Avalonia.LinuxFramebuffer) + "/" + nameof(Avalonia.LinuxFramebuffer.Input) + "/" + nameof(LibInput); private readonly RawEventGroupingThreadingHelper _inputQueue; diff --git a/src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs b/src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs index d881a97af2..38498951f8 100644 --- a/src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs +++ b/src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs @@ -41,7 +41,7 @@ namespace Avalonia.LinuxFramebuffer { Threading = new InternalPlatformThreadingInterface(); if (_fb is IGlOutputBackend gl) - AvaloniaLocator.CurrentMutable.Bind().ToConstant(gl.PlatformOpenGlInterface); + AvaloniaLocator.CurrentMutable.Bind().ToConstant(gl.PlatformGraphics); var opts = AvaloniaLocator.Current.GetService() ?? new LinuxFramebufferPlatformOptions(); @@ -56,7 +56,7 @@ namespace Avalonia.LinuxFramebuffer Compositor = new Compositor( AvaloniaLocator.Current.GetRequiredService(), - AvaloniaLocator.Current.GetService()); + AvaloniaLocator.Current.GetService()); } diff --git a/src/Linux/Avalonia.LinuxFramebuffer/Output/DrmOutput.cs b/src/Linux/Avalonia.LinuxFramebuffer/Output/DrmOutput.cs index a4d7d8b1eb..22dd407791 100644 --- a/src/Linux/Avalonia.LinuxFramebuffer/Output/DrmOutput.cs +++ b/src/Linux/Avalonia.LinuxFramebuffer/Output/DrmOutput.cs @@ -6,6 +6,7 @@ using System.Runtime.InteropServices; using Avalonia.OpenGL; using Avalonia.OpenGL.Egl; using Avalonia.OpenGL.Surfaces; +using Avalonia.Platform; using Avalonia.Platform.Interop; using JetBrains.Annotations; using static Avalonia.LinuxFramebuffer.NativeUnsafeMethods; @@ -25,10 +26,22 @@ namespace Avalonia.LinuxFramebuffer.Output get => _outputOptions.Scaling; set => _outputOptions.Scaling = value; } - public IGlContext PrimaryContext => _deferredContext; - private EglPlatformOpenGlInterface _platformGl; - public IPlatformOpenGlInterface PlatformOpenGlInterface => _platformGl; + class SharedContextGraphics : IPlatformGraphics + { + private readonly IPlatformGraphicsContext _context; + + public SharedContextGraphics(IPlatformGraphicsContext context) + { + _context = context; + } + public bool UsesSharedContext => true; + public IPlatformGraphicsContext CreateContext() => throw new NotSupportedException(); + + public IPlatformGraphicsContext GetSharedContext() => _context; + } + + public IPlatformGraphics PlatformGraphics { get; private set; } public DrmOutput(DrmCard card, DrmResources resources, DrmConnector connector, DrmModeInfo modeInfo, DrmOutputOptions? options = null) @@ -161,11 +174,22 @@ namespace Avalonia.LinuxFramebuffer.Output if(_gbmTargetSurface == IntPtr.Zero) throw new InvalidOperationException("Unable to create GBM surface"); - _eglDisplay = new EglDisplay(new EglInterface(eglGetProcAddress), false, 0x31D7, device, null); - _platformGl = new EglPlatformOpenGlInterface(_eglDisplay); - _eglSurface = _platformGl.CreateWindowSurface(_gbmTargetSurface); + _eglDisplay = new EglDisplay( + new EglDisplayCreationOptions + { + Egl = new EglInterface(eglGetProcAddress), + PlatformType = 0x31D7, + PlatformDisplay = device, + SupportsMultipleContexts = true, + SupportsContextSharing = true + }); + + var surface = _eglDisplay.EglInterface.CreateWindowSurface(_eglDisplay.Handle, _eglDisplay.Config, _gbmTargetSurface, new[] { EglConsts.EGL_NONE, EglConsts.EGL_NONE }); + + _eglSurface = new EglSurface(_eglDisplay, surface); - _deferredContext = _platformGl.PrimaryEglContext; + _deferredContext = _eglDisplay.CreateContext(null); + PlatformGraphics = new SharedContextGraphics(_deferredContext); var initialBufferSwappingColorR = _outputOptions.InitialBufferSwappingColor.R / 255.0f; var initialBufferSwappingColorG = _outputOptions.InitialBufferSwappingColor.G / 255.0f; @@ -206,11 +230,17 @@ namespace Avalonia.LinuxFramebuffer.Output } - public IGlPlatformSurfaceRenderTarget CreateGlRenderTarget() + public IGlPlatformSurfaceRenderTarget CreateGlRenderTarget() => new RenderTarget(this); + + + public IGlPlatformSurfaceRenderTarget CreateGlRenderTarget(IGlContext context) { - return new RenderTarget(this); + if (context != _deferredContext) + throw new InvalidOperationException( + "This platform backend can only create render targets for its primary context"); + return CreateGlRenderTarget(); } - + class RenderTarget : IGlPlatformSurfaceRenderTarget { private readonly DrmOutput _parent; diff --git a/src/Linux/Avalonia.LinuxFramebuffer/Output/IGlOutputBackend.cs b/src/Linux/Avalonia.LinuxFramebuffer/Output/IGlOutputBackend.cs index 7bc73d590c..e415b1782e 100644 --- a/src/Linux/Avalonia.LinuxFramebuffer/Output/IGlOutputBackend.cs +++ b/src/Linux/Avalonia.LinuxFramebuffer/Output/IGlOutputBackend.cs @@ -1,9 +1,10 @@ using Avalonia.OpenGL; +using Avalonia.Platform; namespace Avalonia.LinuxFramebuffer.Output { public interface IGlOutputBackend : IOutputBackend { - public IPlatformOpenGlInterface PlatformOpenGlInterface { get; } + public IPlatformGraphics PlatformGraphics { get; } } } diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/AvaloniaRuntimeXamlLoader.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/AvaloniaRuntimeXamlLoader.cs index b4c951fc5e..6f6420f66d 100644 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/AvaloniaRuntimeXamlLoader.cs +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/AvaloniaRuntimeXamlLoader.cs @@ -56,8 +56,8 @@ namespace Avalonia.Markup.Xaml /// /// Collection of documents. /// Xaml loader configuration. - /// The loaded objects per each input document. - public static IReadOnlyList LoadGroup(IReadOnlyCollection documents, RuntimeXamlLoaderConfiguration? configuration = null) + /// The loaded objects per each input document. If document was removed, the element by index is null. + public static IReadOnlyList LoadGroup(IReadOnlyCollection documents, RuntimeXamlLoaderConfiguration? configuration = null) => AvaloniaXamlIlRuntimeCompiler.LoadGroup(documents, configuration ?? new RuntimeXamlLoaderConfiguration()); /// diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlCompiler.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlCompiler.cs index b8350c3f11..aaaee39b0d 100644 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlCompiler.cs +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlCompiler.cs @@ -85,6 +85,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions GroupTransformers = new() { + new XamlMergeResourceGroupTransformer(), new AvaloniaXamlIncludeTransformer() }; } diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/GroupTransformers/IXamlAstGroupTransformer.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/GroupTransformers/IXamlAstGroupTransformer.cs index 32bf37431f..eeb5293325 100644 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/GroupTransformers/IXamlAstGroupTransformer.cs +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/GroupTransformers/IXamlAstGroupTransformer.cs @@ -18,7 +18,7 @@ internal class AstGroupTransformationContext : AstTransformationContext public IXamlDocumentResource CurrentDocument { get; set; } public IReadOnlyCollection Documents { get; } - + public new IXamlAstNode ParseError(string message, IXamlAstNode node) => Error(node, new XamlDocumentParseException(CurrentDocument?.FileSource?.FilePath, message, node)); diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/GroupTransformers/XamlIncludeGroupTransformer.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/GroupTransformers/XamlIncludeGroupTransformer.cs index 1bb05e238c..afc7b569a8 100644 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/GroupTransformers/XamlIncludeGroupTransformer.cs +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/GroupTransformers/XamlIncludeGroupTransformer.cs @@ -15,6 +15,7 @@ using XamlX.TypeSystem; namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.GroupTransformers; +#nullable enable internal class AvaloniaXamlIncludeTransformer : IXamlAstGroupTransformer { public IXamlAstNode Transform(AstGroupTransformationContext context, IXamlAstNode node) @@ -34,40 +35,26 @@ internal class AvaloniaXamlIncludeTransformer : IXamlAstGroupTransformer { throw new InvalidOperationException($"\"{nodeTypeName}\".Loaded property is expected to be defined"); } - + if (valueNode.Manipulation is not XamlObjectInitializationNode { Manipulation: XamlPropertyAssignmentNode { Property: { Name: "Source" } } sourceProperty }) { - return context.ParseError($"Source property must be set on the \"{nodeTypeName}\" node.", node); + throw new XamlDocumentParseException(context.CurrentDocument, + $"Source property must be set on the \"{nodeTypeName}\" node.", valueNode); } - // We expect that AvaloniaXamlIlLanguageParseIntrinsics has already parsed the Uri and created node like: `new Uri(assetPath, uriKind)`. - if (sourceProperty.Values.OfType().FirstOrDefault() is not { } sourceUriNode - || sourceUriNode.Type.GetClrType() != context.GetAvaloniaTypes().Uri - || sourceUriNode.Arguments.FirstOrDefault() is not XamlConstantNode { Constant: string originalAssetPath } - || sourceUriNode.Arguments.Skip(1).FirstOrDefault() is not XamlConstantNode { Constant: int uriKind }) + var (assetPathUri, sourceUriNode) = ResolveSourceFromXamlInclude(context, nodeTypeName, sourceProperty, false); + if (assetPathUri is null) { - // TODO: make it a compiler warning - // Source value can be set with markup extension instead of the Uri object node, we don't support it here yet. return node; } - - var uriPath = new Uri(originalAssetPath, (UriKind)uriKind); - if (!uriPath.IsAbsoluteUri) + else { - var baseUrl = context.CurrentDocument.Uri ?? throw new InvalidOperationException("CurrentDocument URI is null."); - uriPath = new Uri(new Uri(baseUrl, UriKind.Absolute), uriPath); - } - else if (!uriPath.Scheme.Equals("avares", StringComparison.CurrentCultureIgnoreCase)) - { - return context.ParseError( - $"\"{nodeTypeName}.Source\" supports only \"avares://\" absolute or relative uri.", - sourceUriNode, node); + sourceUriNode ??= valueNode; } - var assetPathUri = Uri.UnescapeDataString(uriPath.AbsoluteUri); var assetPath = assetPathUri.Replace("avares://", ""); var assemblyNameSeparator = assetPath.IndexOf('/'); var assembly = assetPath.Substring(0, assemblyNameSeparator); @@ -119,7 +106,7 @@ internal class AvaloniaXamlIncludeTransformer : IXamlAstGroupTransformer $"Unable to resolve XAML resource \"{assetPathUri}\" in the \"{assembly}\" assembly.", sourceUriNode, node); } - + private static IXamlAstNode FromType(AstTransformationContext context, IXamlType type, IXamlAstNode li, IXamlType expectedLoadedType, IXamlAstNode fallbackNode, string assetPathUri, string assembly) { @@ -151,7 +138,49 @@ internal class AvaloniaXamlIncludeTransformer : IXamlAstGroupTransformer new[] { new NewServiceProviderNode(sp, li) }); } - internal class NewServiceProviderNode : XamlAstNode, IXamlAstValueNode,IXamlAstNodeNeedsParentStack, + internal static (string?, IXamlAstNode?) ResolveSourceFromXamlInclude( + AstGroupTransformationContext context, string nodeTypeName, XamlPropertyAssignmentNode sourceProperty, + bool strictSourceValueType) + { + // We expect that AvaloniaXamlIlLanguageParseIntrinsics has already parsed the Uri and created node like: `new Uri(assetPath, uriKind)`. + if (sourceProperty.Values.OfType().FirstOrDefault() is not { } sourceUriNode + || sourceUriNode.Type.GetClrType() != context.GetAvaloniaTypes().Uri + || sourceUriNode.Arguments.FirstOrDefault() is not XamlConstantNode { Constant: string originalAssetPath } + || sourceUriNode.Arguments.Skip(1).FirstOrDefault() is not XamlConstantNode { Constant: int uriKind }) + { + // Source value can be set with markup extension instead of the Uri object node, we don't support it here yet. + var anyPropValue = sourceProperty.Values.FirstOrDefault(); + if (strictSourceValueType) + { + context.Error(anyPropValue, + new XamlDocumentParseException(context.CurrentDocument, + $"\"{nodeTypeName}.Source\" supports only \"avares://\" absolute or relative uri.", anyPropValue)); + } + else + { + // TODO: make it a compiler warning + } + return (null, anyPropValue); + } + + var uriPath = new Uri(originalAssetPath, (UriKind)uriKind); + if (!uriPath.IsAbsoluteUri) + { + var baseUrl = context.CurrentDocument.Uri ?? throw new InvalidOperationException("CurrentDocument URI is null."); + uriPath = new Uri(new Uri(baseUrl, UriKind.Absolute), uriPath); + } + else if (!uriPath.Scheme.Equals("avares", StringComparison.CurrentCultureIgnoreCase)) + { + context.Error(sourceUriNode, + new XamlDocumentParseException(context.CurrentDocument, + $"\"{nodeTypeName}.Source\" supports only \"avares://\" absolute or relative uri.", sourceUriNode)); + return (null, sourceUriNode); + } + + return (Uri.UnescapeDataString(uriPath.AbsoluteUri), sourceUriNode); + } + + private class NewServiceProviderNode : XamlAstNode, IXamlAstValueNode,IXamlAstNodeNeedsParentStack, IXamlAstEmitableNode { public NewServiceProviderNode(IXamlType type, IXamlLineInfo lineInfo) : base(lineInfo) diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/GroupTransformers/XamlMergeResourceGroupTransformer.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/GroupTransformers/XamlMergeResourceGroupTransformer.cs new file mode 100644 index 0000000000..8c83c74248 --- /dev/null +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/GroupTransformers/XamlMergeResourceGroupTransformer.cs @@ -0,0 +1,113 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers; +using XamlX.Ast; +using XamlX.IL.Emitters; + +namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.GroupTransformers; +#nullable enable + +internal class XamlMergeResourceGroupTransformer : IXamlAstGroupTransformer +{ + public IXamlAstNode Transform(AstGroupTransformationContext context, IXamlAstNode node) + { + var resourceDictionaryType = context.GetAvaloniaTypes().ResourceDictionary; + if (node is not XamlObjectInitializationNode resourceDictionaryNode + || resourceDictionaryNode.Type != resourceDictionaryType + || resourceDictionaryNode.Manipulation is not XamlManipulationGroupNode resourceDictionaryManipulation) + { + return node; + } + + var mergeResourceIncludeType = context.GetAvaloniaTypes().MergeResourceInclude; + var mergeSourceNodes = new List(); + var hasAnyNonMergedResource = false; + foreach (var manipulationNode in resourceDictionaryManipulation.Children.ToArray()) + { + void ProcessXamlPropertyAssignmentNode(XamlManipulationGroupNode parent, XamlPropertyAssignmentNode assignmentNode) + { + if (assignmentNode.Property.Name == "MergedDictionaries" + && assignmentNode.Values.FirstOrDefault() is XamlValueWithManipulationNode valueNode) + { + if (valueNode.Type.GetClrType() == mergeResourceIncludeType) + { + if (valueNode.Manipulation is XamlObjectInitializationNode objectInitialization + && objectInitialization.Manipulation is XamlPropertyAssignmentNode sourceAssignmentNode) + { + parent.Children.Remove(assignmentNode); + mergeSourceNodes.Add(sourceAssignmentNode); + } + else + { + throw new XamlDocumentParseException(context.CurrentDocument, + "Invalid MergeResourceInclude node found. Make sure that Source property is set.", + valueNode); + } + } + else + { + hasAnyNonMergedResource = true; + } + + if (hasAnyNonMergedResource && mergeSourceNodes.Any()) + { + throw new XamlDocumentParseException(context.CurrentDocument, + "Mix of MergeResourceInclude and other dictionaries inside of the ResourceDictionary.MergedDictionaries is not allowed", + valueNode); + } + } + } + + if (manipulationNode is XamlPropertyAssignmentNode singleValueAssignment) + { + ProcessXamlPropertyAssignmentNode(resourceDictionaryManipulation, singleValueAssignment); + } + else if (manipulationNode is XamlManipulationGroupNode groupNodeValues) + { + foreach (var groupNodeValue in groupNodeValues.Children.OfType().ToArray()) + { + ProcessXamlPropertyAssignmentNode(groupNodeValues, groupNodeValue); + } + } + } + + var manipulationGroup = new XamlManipulationGroupNode(node, new List()); + foreach (var sourceNode in mergeSourceNodes) + { + var (originalAssetPath, propertyNode) = + AvaloniaXamlIncludeTransformer.ResolveSourceFromXamlInclude(context, "MergeResourceInclude", sourceNode, true); + if (originalAssetPath is null) + { + return node; + } + + var targetDocument = context.Documents.FirstOrDefault(d => + string.Equals(d.Uri, originalAssetPath, StringComparison.InvariantCultureIgnoreCase)) + ?.XamlDocument.Root as XamlValueWithManipulationNode; + if (targetDocument is null) + { + return context.ParseError( + $"Node MergeResourceInclude is unable to resolve \"{originalAssetPath}\" path.", propertyNode, node); + } + + var singleRootObject = ((XamlManipulationGroupNode)targetDocument.Manipulation) + .Children.OfType().Single(); + if (singleRootObject.Type != resourceDictionaryType) + { + return context.ParseError( + $"MergeResourceInclude can only include another ResourceDictionary", propertyNode, node); + } + + manipulationGroup.Children.Add(singleRootObject.Manipulation); + } + + if (manipulationGroup.Children.Any()) + { + // MergedDictionaries are read first, so we need ot inject our merged values in the beginning. + resourceDictionaryManipulation.Children.Insert(0, manipulationGroup); + } + + return node; + } +} diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs index 87a037c16a..5753a1008c 100644 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs @@ -104,6 +104,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers public IXamlType IStyle { get; } public IXamlType StyleInclude { get; } public IXamlType ResourceInclude { get; } + public IXamlType MergeResourceInclude { get; } public IXamlType IResourceDictionary { get; } public IXamlType ResourceDictionary { get; } public IXamlMethod ResourceDictionaryDeferredAdd { get; } @@ -236,6 +237,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers IStyle = cfg.TypeSystem.GetType("Avalonia.Styling.IStyle"); StyleInclude = cfg.TypeSystem.GetType("Avalonia.Markup.Xaml.Styling.StyleInclude"); ResourceInclude = cfg.TypeSystem.GetType("Avalonia.Markup.Xaml.Styling.ResourceInclude"); + MergeResourceInclude = cfg.TypeSystem.GetType("Avalonia.Markup.Xaml.Styling.MergeResourceInclude"); IResourceDictionary = cfg.TypeSystem.GetType("Avalonia.Controls.IResourceDictionary"); ResourceDictionary = cfg.TypeSystem.GetType("Avalonia.Controls.ResourceDictionary"); ResourceDictionaryDeferredAdd = ResourceDictionary.FindMethod("AddDeferred", XamlIlTypes.Void, true, XamlIlTypes.Object, diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/XamlDocumentParseException.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/XamlDocumentParseException.cs index d031a6086b..0532287a67 100644 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/XamlDocumentParseException.cs +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/XamlDocumentParseException.cs @@ -18,4 +18,9 @@ internal class XamlDocumentParseException : XamlParseException { FilePath = path; } + + public XamlDocumentParseException(IXamlDocumentResource document, string message, IXamlLineInfo lineInfo) + : this(document.FileSource?.FilePath, message, lineInfo) + { + } } diff --git a/src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj b/src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj index c59542c0f3..267bd2520e 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj +++ b/src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj @@ -43,6 +43,7 @@ + diff --git a/src/Markup/Avalonia.Markup.Xaml/Styling/MergeResourceInclude.cs b/src/Markup/Avalonia.Markup.Xaml/Styling/MergeResourceInclude.cs new file mode 100644 index 0000000000..d231a1d221 --- /dev/null +++ b/src/Markup/Avalonia.Markup.Xaml/Styling/MergeResourceInclude.cs @@ -0,0 +1,26 @@ +using System; +using System.Diagnostics.CodeAnalysis; +using Avalonia.Controls; + +#nullable enable + +namespace Avalonia.Markup.Xaml.Styling; + +/// +/// Loads a resource dictionary from a specified URL. +/// +/// +/// If used from the XAML code, it is merged into the parent dictionary in the compile time. +/// When used in runtime, this type behaves like . +/// +[RequiresUnreferencedCode(TrimmingMessages.StyleResourceIncludeRequiresUnreferenceCodeMessage)] +public class MergeResourceInclude : ResourceInclude +{ + public MergeResourceInclude(Uri? baseUri) : base(baseUri) + { + } + + public MergeResourceInclude(IServiceProvider serviceProvider) : base(serviceProvider) + { + } +} diff --git a/src/Markup/Avalonia.Markup.Xaml/Styling/ResourceInclude.cs b/src/Markup/Avalonia.Markup.Xaml/Styling/ResourceInclude.cs index 3d8a61364a..595b37f7d1 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Styling/ResourceInclude.cs +++ b/src/Markup/Avalonia.Markup.Xaml/Styling/ResourceInclude.cs @@ -9,6 +9,10 @@ namespace Avalonia.Markup.Xaml.Styling /// /// Loads a resource dictionary from a specified URL. /// + /// + /// If used from the XAML code, it is replaced with direct resource dictionary reference. + /// When used in runtime, this type might be unsafe with trimming and AOT. + /// [RequiresUnreferencedCode(TrimmingMessages.StyleResourceIncludeRequiresUnreferenceCodeMessage)] public class ResourceInclude : IResourceProvider { diff --git a/src/Markup/Avalonia.Markup.Xaml/Styling/StyleInclude.cs b/src/Markup/Avalonia.Markup.Xaml/Styling/StyleInclude.cs index bdde6e725c..b87aa64297 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Styling/StyleInclude.cs +++ b/src/Markup/Avalonia.Markup.Xaml/Styling/StyleInclude.cs @@ -11,6 +11,10 @@ namespace Avalonia.Markup.Xaml.Styling /// /// Includes a style from a URL. /// + /// + /// If used from the XAML code, it is replaced with direct style reference. + /// When used in runtime, this type might be unsafe with trimming and AOT. + /// [RequiresUnreferencedCode(TrimmingMessages.StyleResourceIncludeRequiresUnreferenceCodeMessage)] public class StyleInclude : IStyle, IResourceProvider { diff --git a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs index f18330ba8a..a7bec62366 100644 --- a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs +++ b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs @@ -362,7 +362,7 @@ namespace Avalonia.Skia foreach (var boxShadow in boxShadows) { - if (!boxShadow.IsEmpty && !boxShadow.IsInset) + if (!boxShadow.IsDefault && !boxShadow.IsInset) { using (var shadow = BoxShadowFilter.Create(_boxShadowPaint, boxShadow, _currentOpacity)) { @@ -418,7 +418,7 @@ namespace Avalonia.Skia foreach (var boxShadow in boxShadows) { - if (!boxShadow.IsEmpty && boxShadow.IsInset) + if (!boxShadow.IsDefault && boxShadow.IsInset) { using (var shadow = BoxShadowFilter.Create(_boxShadowPaint, boxShadow, _currentOpacity)) { @@ -1137,11 +1137,14 @@ namespace Avalonia.Skia if (pen.DashStyle?.Dashes != null && pen.DashStyle.Dashes.Count > 0) { var srcDashes = pen.DashStyle.Dashes; - var dashesArray = new float[srcDashes.Count]; - for (var i = 0; i < srcDashes.Count; ++i) + var count = srcDashes.Count % 2 == 0 ? srcDashes.Count : srcDashes.Count * 2; + + var dashesArray = new float[count]; + + for (var i = 0; i < count; ++i) { - dashesArray[i] = (float) srcDashes[i] * paint.StrokeWidth; + dashesArray[i] = (float) srcDashes[i % srcDashes.Count] * paint.StrokeWidth; } var offset = (float)(pen.DashStyle.Offset * pen.Thickness); diff --git a/src/Skia/Avalonia.Skia/FramebufferRenderTarget.cs b/src/Skia/Avalonia.Skia/FramebufferRenderTarget.cs index 9dbfd0f8d9..1ae47c8b7a 100644 --- a/src/Skia/Avalonia.Skia/FramebufferRenderTarget.cs +++ b/src/Skia/Avalonia.Skia/FramebufferRenderTarget.cs @@ -61,6 +61,8 @@ namespace Avalonia.Skia return new DrawingContextImpl(createInfo, _preFramebufferCopyHandler, canvas, framebuffer); } + public bool IsCorrupted => false; + /// /// Check if two images info are compatible. /// diff --git a/src/Skia/Avalonia.Skia/GeometryImpl.cs b/src/Skia/Avalonia.Skia/GeometryImpl.cs index 4037cc4a35..1141763097 100644 --- a/src/Skia/Avalonia.Skia/GeometryImpl.cs +++ b/src/Skia/Avalonia.Skia/GeometryImpl.cs @@ -244,7 +244,7 @@ namespace Avalonia.Skia public void Invalidate() { CachedStrokePath?.Dispose(); - CachedGeometryRenderBounds = Rect.Empty; + CachedGeometryRenderBounds = default; _cachedStrokeWidth = default(float); } } diff --git a/src/Skia/Avalonia.Skia/Gpu/ISkiaGpu.cs b/src/Skia/Avalonia.Skia/Gpu/ISkiaGpu.cs index 4a7d4d4ac8..b064445a0b 100644 --- a/src/Skia/Avalonia.Skia/Gpu/ISkiaGpu.cs +++ b/src/Skia/Avalonia.Skia/Gpu/ISkiaGpu.cs @@ -1,6 +1,8 @@ using System; using System.Collections.Generic; +using Avalonia.OpenGL; using Avalonia.OpenGL.Imaging; +using Avalonia.Platform; using SkiaSharp; namespace Avalonia.Skia @@ -8,7 +10,7 @@ namespace Avalonia.Skia /// /// Custom Skia gpu instance. /// - public interface ISkiaGpu + public interface ISkiaGpu : IPlatformGraphicsContext { /// /// Attempts to create custom render target from given surfaces. diff --git a/src/Skia/Avalonia.Skia/Gpu/OpenGl/FboSkiaSurface.cs b/src/Skia/Avalonia.Skia/Gpu/OpenGl/FboSkiaSurface.cs index 5cc338a469..e19379df09 100644 --- a/src/Skia/Avalonia.Skia/Gpu/OpenGl/FboSkiaSurface.cs +++ b/src/Skia/Avalonia.Skia/Gpu/OpenGl/FboSkiaSurface.cs @@ -1,11 +1,13 @@ using System; using Avalonia.OpenGL; +using Avalonia.Platform; using SkiaSharp; using static Avalonia.OpenGL.GlConsts; namespace Avalonia.Skia { - public class FboSkiaSurface : ISkiaSurface + internal class FboSkiaSurface : ISkiaSurface { + private readonly GlSkiaGpu _gpu; private readonly GRContext _grContext; private readonly IGlContext _glContext; private readonly PixelSize _pixelSize; @@ -14,8 +16,9 @@ namespace Avalonia.Skia private int _texture; private static readonly bool[] TrueFalse = new[] { true, false }; - public FboSkiaSurface(GRContext grContext, IGlContext glContext, PixelSize pixelSize, GRSurfaceOrigin surfaceOrigin) + public FboSkiaSurface(GlSkiaGpu gpu, GRContext grContext, IGlContext glContext, PixelSize pixelSize, GRSurfaceOrigin surfaceOrigin) { + _gpu = gpu; _grContext = grContext; _glContext = glContext; _pixelSize = pixelSize; @@ -93,19 +96,33 @@ namespace Avalonia.Skia public void Dispose() { - using (_glContext.EnsureCurrent()) + try { - Surface?.Dispose(); - Surface = null; - var gl = _glContext.GlInterface; - if (_fbo != 0) + using (_glContext.EnsureCurrent()) { - gl.DeleteFramebuffer(_fbo); - gl.DeleteTexture(_texture); - gl.DeleteRenderbuffer(_depthStencil); - _fbo = _texture = _depthStencil = 0; + Surface?.Dispose(); + Surface = null; + var gl = _glContext.GlInterface; + if (_fbo != 0) + { + gl.DeleteFramebuffer(_fbo); + gl.DeleteTexture(_texture); + gl.DeleteRenderbuffer(_depthStencil); + } } } + catch (PlatformGraphicsContextLostException) + { + if (Surface != null) + // We need to dispose SKSurface _after_ GRContext.Abandon was called, + // otherwise it will try to do OpenGL calls without a proper context + _gpu.AddPostDispose(Surface.Dispose); + Surface = null; + } + finally + { + _fbo = _texture = _depthStencil = 0; + } } public SKSurface Surface { get; private set; } diff --git a/src/Skia/Avalonia.Skia/Gpu/OpenGl/GlRenderTarget.cs b/src/Skia/Avalonia.Skia/Gpu/OpenGl/GlRenderTarget.cs index 476ecc4a39..5bf1272c2f 100644 --- a/src/Skia/Avalonia.Skia/Gpu/OpenGl/GlRenderTarget.cs +++ b/src/Skia/Avalonia.Skia/Gpu/OpenGl/GlRenderTarget.cs @@ -14,10 +14,11 @@ namespace Avalonia.Skia private readonly GRContext _grContext; private IGlPlatformSurfaceRenderTarget _surface; - public GlRenderTarget(GRContext grContext, IGlPlatformSurface glSurface) + public GlRenderTarget(GRContext grContext, IGlContext glContext, IGlPlatformSurface glSurface) { _grContext = grContext; - _surface = glSurface.CreateGlRenderTarget(); + using (glContext.EnsureCurrent()) + _surface = glSurface.CreateGlRenderTarget(glContext); } public void Dispose() => _surface.Dispose(); diff --git a/src/Skia/Avalonia.Skia/Gpu/OpenGl/GlSkiaGpu.cs b/src/Skia/Avalonia.Skia/Gpu/OpenGl/GlSkiaGpu.cs index 08e0fbd808..cdba3b9ea2 100644 --- a/src/Skia/Avalonia.Skia/Gpu/OpenGl/GlSkiaGpu.cs +++ b/src/Skia/Avalonia.Skia/Gpu/OpenGl/GlSkiaGpu.cs @@ -5,21 +5,22 @@ using Avalonia.Logging; using Avalonia.OpenGL; using Avalonia.OpenGL.Imaging; using Avalonia.OpenGL.Surfaces; +using Avalonia.Platform; using SkiaSharp; namespace Avalonia.Skia { - class GlSkiaGpu : IOpenGlAwareSkiaGpu + class GlSkiaGpu : IOpenGlAwareSkiaGpu, IOpenGlTextureSharingRenderInterfaceContextFeature { private GRContext _grContext; private IGlContext _glContext; + private List _postDisposeCallbacks = new(); private bool? _canCreateSurfaces; - public GlSkiaGpu(IPlatformOpenGlInterface openGl, long? maxResourceBytes) + public GlSkiaGpu(IGlContext context, long? maxResourceBytes) { - var context = openGl.PrimaryContext; _glContext = context; - using (context.MakeCurrent()) + using (_glContext.EnsureCurrent()) { using (var iface = context.Version.Type == GlProfileType.OpenGL ? GRGlInterface.CreateOpenGl(proc => context.GlInterface.GetProcAddress(proc)) : @@ -34,13 +35,34 @@ namespace Avalonia.Skia } } + class SurfaceWrapper : IGlPlatformSurface + { + private readonly object _surface; + + public SurfaceWrapper( object surface) + { + _surface = surface; + } + + public IGlPlatformSurfaceRenderTarget CreateGlRenderTarget(IGlContext context) + { + var feature = context.TryGetFeature()!; + return feature.CreateRenderTarget(context, _surface); + } + } + public ISkiaGpuRenderTarget TryCreateRenderTarget(IEnumerable surfaces) { + var customRenderTargetFactory = _glContext.TryGetFeature(); foreach (var surface in surfaces) { + if (customRenderTargetFactory?.CanRenderToSurface(_glContext, surface) == true) + { + return new GlRenderTarget(_grContext, _glContext, new SurfaceWrapper(surface)); + } if (surface is IGlPlatformSurface glSurface) { - return new GlRenderTarget(_grContext, glSurface); + return new GlRenderTarget(_grContext, _glContext, glSurface); } } @@ -62,7 +84,8 @@ namespace Avalonia.Skia return null; try { - var surface = new FboSkiaSurface(_grContext, _glContext, size, session?.SurfaceOrigin ?? GRSurfaceOrigin.TopLeft); + var surface = new FboSkiaSurface(this, _grContext, _glContext, size, + session?.SurfaceOrigin ?? GRSurfaceOrigin.TopLeft); _canCreateSurfaces = true; return surface; } @@ -75,6 +98,40 @@ namespace Avalonia.Skia } } + public bool CanCreateSharedContext => _glContext.CanCreateSharedContext; + + public IGlContext CreateSharedContext(IEnumerable preferredVersions = null) => + _glContext.CreateSharedContext(preferredVersions); + public IOpenGlBitmapImpl CreateOpenGlBitmap(PixelSize size, Vector dpi) => new GlOpenGlBitmapImpl(_glContext, size, dpi); + + public void Dispose() + { + if (_glContext.IsLost) + _grContext.AbandonContext(); + else + _grContext.AbandonContext(true); + _grContext.Dispose(); + + lock(_postDisposeCallbacks) + foreach (var cb in _postDisposeCallbacks) + cb(); + } + + public bool IsLost => _glContext.IsLost; + public IDisposable EnsureCurrent() => _glContext.EnsureCurrent(); + + public object TryGetFeature(Type featureType) + { + if (featureType == typeof(IOpenGlTextureSharingRenderInterfaceContextFeature)) + return this; + return null; + } + + public void AddPostDispose(Action dispose) + { + lock (_postDisposeCallbacks) + _postDisposeCallbacks.Add(dispose); + } } } diff --git a/src/Skia/Avalonia.Skia/Gpu/OpenGl/OpenGlBitmapImpl.cs b/src/Skia/Avalonia.Skia/Gpu/OpenGl/OpenGlBitmapImpl.cs index 5e02500bf0..d8bff7cfc8 100644 --- a/src/Skia/Avalonia.Skia/Gpu/OpenGl/OpenGlBitmapImpl.cs +++ b/src/Skia/Avalonia.Skia/Gpu/OpenGl/OpenGlBitmapImpl.cs @@ -109,7 +109,9 @@ namespace Avalonia.Skia using (_context.EnsureCurrent()) { var glVersion = _context.Version; - InternalFormat = glVersion.Type == GlProfileType.OpenGLES ? GL_RGBA : GL_RGBA8; + InternalFormat = glVersion.Type == GlProfileType.OpenGLES && glVersion.Major == 2 + ? GL_RGBA + : GL_RGBA8; _context.GlInterface.GetIntegerv(GL_FRAMEBUFFER_BINDING, out _fbo); if (_fbo == 0) @@ -145,7 +147,7 @@ namespace Avalonia.Skia public void Present() { - using (_context.MakeCurrent()) + using (_context.EnsureCurrent()) { if (_disposed) throw new ObjectDisposedException(nameof(SharedOpenGlBitmapAttachment)); diff --git a/src/Skia/Avalonia.Skia/Gpu/SkiaGpuRenderTarget.cs b/src/Skia/Avalonia.Skia/Gpu/SkiaGpuRenderTarget.cs index 6626546c0c..6b4a7a3409 100644 --- a/src/Skia/Avalonia.Skia/Gpu/SkiaGpuRenderTarget.cs +++ b/src/Skia/Avalonia.Skia/Gpu/SkiaGpuRenderTarget.cs @@ -6,7 +6,7 @@ namespace Avalonia.Skia /// /// Adapts to be used within our rendering pipeline. /// - internal class SkiaGpuRenderTarget : IRenderTargetWithCorruptionInfo + internal class SkiaGpuRenderTarget : IRenderTarget { private readonly ISkiaGpu _skiaGpu; private readonly ISkiaGpuRenderTarget _renderTarget; diff --git a/src/Skia/Avalonia.Skia/Helpers/ImageSavingHelper.cs b/src/Skia/Avalonia.Skia/Helpers/ImageSavingHelper.cs index b4dd754822..4cb1430a3b 100644 --- a/src/Skia/Avalonia.Skia/Helpers/ImageSavingHelper.cs +++ b/src/Skia/Avalonia.Skia/Helpers/ImageSavingHelper.cs @@ -34,6 +34,7 @@ namespace Avalonia.Skia.Helpers /// Save Skia image to a stream. /// /// Image to save + /// The output stream to save the image. /// /// The optional quality for PNG compression. /// The quality value is interpreted from 0 - 100. If quality is null diff --git a/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs b/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs index f34e25299c..b202b60cdf 100644 --- a/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs +++ b/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs @@ -18,26 +18,28 @@ namespace Avalonia.Skia /// /// Skia platform render interface. /// - internal class PlatformRenderInterface : IPlatformRenderInterface, IOpenGlAwarePlatformRenderInterface + internal class PlatformRenderInterface : IPlatformRenderInterface { - private readonly ISkiaGpu _skiaGpu; + private readonly long? _maxResourceBytes; - public PlatformRenderInterface(ISkiaGpu skiaGpu, long? maxResourceBytes = null) + public PlatformRenderInterface(long? maxResourceBytes = null) { + _maxResourceBytes = maxResourceBytes; DefaultPixelFormat = SKImageInfo.PlatformColorType.ToPixelFormat(); + } - if (skiaGpu != null) - { - _skiaGpu = skiaGpu; - return; - } - var gl = AvaloniaLocator.Current.GetService(); - if (gl != null) - _skiaGpu = new GlSkiaGpu(gl, maxResourceBytes); + public IPlatformRenderInterfaceContext CreateBackendContext(IPlatformGraphicsContext graphicsContext) + { + if (graphicsContext == null) + return new SkiaContext(null); + if (graphicsContext is ISkiaGpu skiaGpu) + return new SkiaContext(skiaGpu); + if (graphicsContext is IGlContext gl) + return new SkiaContext(new GlSkiaGpu(gl, _maxResourceBytes)); + throw new ArgumentException("Graphics context of type is not supported"); } - public bool SupportsIndividualRoundRects => true; public AlphaFormat DefaultAlphaFormat => AlphaFormat.Premul; @@ -205,43 +207,12 @@ namespace Avalonia.Skia return new SurfaceRenderTarget(createInfo); } - /// - public IRenderTarget CreateRenderTarget(IEnumerable surfaces) - { - if (!(surfaces is IList)) - surfaces = surfaces.ToList(); - var gpuRenderTarget = _skiaGpu?.TryCreateRenderTarget(surfaces); - if (gpuRenderTarget != null) - { - return new SkiaGpuRenderTarget(_skiaGpu, gpuRenderTarget); - } - - foreach (var surface in surfaces) - { - if (surface is IFramebufferPlatformSurface framebufferSurface) - return new FramebufferRenderTarget(framebufferSurface); - } - - throw new NotSupportedException( - "Don't know how to create a Skia render target from any of provided surfaces"); - } - /// public IWriteableBitmapImpl CreateWriteableBitmap(PixelSize size, Vector dpi, PixelFormat format, AlphaFormat alphaFormat) { return new WriteableBitmapImpl(size, dpi, format, alphaFormat); } - public IOpenGlBitmapImpl CreateOpenGlBitmap(PixelSize size, Vector dpi) - { - if (_skiaGpu is IOpenGlAwareSkiaGpu glAware) - return glAware.CreateOpenGlBitmap(size, dpi); - if (_skiaGpu == null) - throw new PlatformNotSupportedException("GPU acceleration is not available"); - throw new PlatformNotSupportedException( - "Current GPU acceleration backend does not support OpenGL integration"); - } - public IGlyphRunImpl CreateGlyphRun(IGlyphTypeface glyphTypeface, double fontRenderingEmSize, IReadOnlyList glyphIndices, IReadOnlyList glyphAdvances, IReadOnlyList glyphOffsets) { diff --git a/src/Skia/Avalonia.Skia/SkiaBackendContext.cs b/src/Skia/Avalonia.Skia/SkiaBackendContext.cs new file mode 100644 index 0000000000..4949f4a50d --- /dev/null +++ b/src/Skia/Avalonia.Skia/SkiaBackendContext.cs @@ -0,0 +1,47 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using Avalonia.Controls.Platform.Surfaces; +using Avalonia.Platform; + +namespace Avalonia.Skia; + +internal class SkiaContext : IPlatformRenderInterfaceContext +{ + private ISkiaGpu _gpu; + + public SkiaContext(ISkiaGpu gpu) + { + _gpu = gpu; + } + + public void Dispose() + { + _gpu?.Dispose(); + _gpu = null; + } + + /// + public IRenderTarget CreateRenderTarget(IEnumerable surfaces) + { + if (!(surfaces is IList)) + surfaces = surfaces.ToList(); + var gpuRenderTarget = _gpu?.TryCreateRenderTarget(surfaces); + if (gpuRenderTarget != null) + { + return new SkiaGpuRenderTarget(_gpu, gpuRenderTarget); + } + + foreach (var surface in surfaces) + { + if (surface is IFramebufferPlatformSurface framebufferSurface) + return new FramebufferRenderTarget(framebufferSurface); + } + + throw new NotSupportedException( + "Don't know how to create a Skia render target from any of provided surfaces"); + } + + public object TryGetFeature(Type featureType) => _gpu?.TryGetFeature(featureType); +} \ No newline at end of file diff --git a/src/Skia/Avalonia.Skia/SkiaOptions.cs b/src/Skia/Avalonia.Skia/SkiaOptions.cs index 493263677d..b3c3056a58 100644 --- a/src/Skia/Avalonia.Skia/SkiaOptions.cs +++ b/src/Skia/Avalonia.Skia/SkiaOptions.cs @@ -8,11 +8,6 @@ namespace Avalonia /// public class SkiaOptions { - /// - /// Custom gpu factory to use. Can be used to customize behavior of Skia renderer. - /// - public Func CustomGpuFactory { get; set; } - /// /// The maximum number of bytes for video memory to store textures and resources. /// diff --git a/src/Skia/Avalonia.Skia/SkiaPlatform.cs b/src/Skia/Avalonia.Skia/SkiaPlatform.cs index 9a5725e06f..27f2631db8 100644 --- a/src/Skia/Avalonia.Skia/SkiaPlatform.cs +++ b/src/Skia/Avalonia.Skia/SkiaPlatform.cs @@ -17,8 +17,7 @@ namespace Avalonia.Skia public static void Initialize(SkiaOptions options) { - var customGpu = options.CustomGpuFactory?.Invoke(); - var renderInterface = new PlatformRenderInterface(customGpu, options.MaxGpuResourceSizeBytes); + var renderInterface = new PlatformRenderInterface(options.MaxGpuResourceSizeBytes); AvaloniaLocator.CurrentMutable .Bind().ToConstant(renderInterface) diff --git a/src/Skia/Avalonia.Skia/StreamGeometryImpl.cs b/src/Skia/Avalonia.Skia/StreamGeometryImpl.cs index 86450690e6..df847d2224 100644 --- a/src/Skia/Avalonia.Skia/StreamGeometryImpl.cs +++ b/src/Skia/Avalonia.Skia/StreamGeometryImpl.cs @@ -34,7 +34,7 @@ namespace Avalonia.Skia /// /// Initializes a new instance of the class. /// - public StreamGeometryImpl() : this(CreateEmptyPath(), Rect.Empty) + public StreamGeometryImpl() : this(CreateEmptyPath(), default) { } diff --git a/src/Skia/Avalonia.Skia/SurfaceRenderTarget.cs b/src/Skia/Avalonia.Skia/SurfaceRenderTarget.cs index 74e3ebc51d..a7998353d9 100644 --- a/src/Skia/Avalonia.Skia/SurfaceRenderTarget.cs +++ b/src/Skia/Avalonia.Skia/SurfaceRenderTarget.cs @@ -107,6 +107,8 @@ namespace Avalonia.Skia return new DrawingContextImpl(createInfo, Disposable.Create(() => Version++)); } + public bool IsCorrupted => _gpu?.IsLost == true; + /// public Vector Dpi { get; } diff --git a/src/Skia/Avalonia.Skia/TextShaperImpl.cs b/src/Skia/Avalonia.Skia/TextShaperImpl.cs index eaf588c27d..98eb35d5c5 100644 --- a/src/Skia/Avalonia.Skia/TextShaperImpl.cs +++ b/src/Skia/Avalonia.Skia/TextShaperImpl.cs @@ -3,7 +3,6 @@ using System.Globalization; using Avalonia.Media.TextFormatting; using Avalonia.Media.TextFormatting.Unicode; using Avalonia.Platform; -using Avalonia.Utilities; using HarfBuzzSharp; using Buffer = HarfBuzzSharp.Buffer; using GlyphInfo = HarfBuzzSharp.GlyphInfo; @@ -12,8 +11,9 @@ namespace Avalonia.Skia { internal class TextShaperImpl : ITextShaperImpl { - public ShapedBuffer ShapeText(ReadOnlySlice text, TextShaperOptions options) + public ShapedBuffer ShapeText(CharacterBufferReference characterBufferReference, int length, TextShaperOptions options) { + var text = new CharacterBufferRange(characterBufferReference, length); var typeface = options.Typeface; var fontRenderingEmSize = options.FontRenderingEmSize; var bidiLevel = options.BidiLevel; @@ -21,21 +21,21 @@ namespace Avalonia.Skia using (var buffer = new Buffer()) { - buffer.AddUtf16(text.Buffer.Span, text.BufferOffset, text.Length); + buffer.AddUtf16(characterBufferReference.CharacterBuffer.Span, characterBufferReference.OffsetToFirstChar, length); MergeBreakPair(buffer); - + buffer.GuessSegmentProperties(); buffer.Direction = (bidiLevel & 1) == 0 ? Direction.LeftToRight : Direction.RightToLeft; - buffer.Language = new Language(culture ?? CultureInfo.CurrentCulture); + buffer.Language = new Language(culture ?? CultureInfo.CurrentCulture); var font = ((GlyphTypefaceImpl)typeface).Font; font.Shape(buffer); - if(buffer.Direction == Direction.RightToLeft) + if (buffer.Direction == Direction.RightToLeft) { buffer.Reverse(); } @@ -64,12 +64,12 @@ namespace Avalonia.Skia var glyphOffset = GetGlyphOffset(glyphPositions, i, textScale); - if(text.Buffer.Span[glyphCluster] == '\t') + if (text[i] == '\t') { glyphIndex = typeface.GetGlyph(' '); - glyphAdvance = options.IncrementalTabWidth > 0 ? - options.IncrementalTabWidth : + glyphAdvance = options.IncrementalTabWidth > 0 ? + options.IncrementalTabWidth : 4 * typeface.GetGlyphAdvance(glyphIndex) * textScale; } @@ -87,7 +87,7 @@ namespace Avalonia.Skia var length = buffer.Length; var glyphInfos = buffer.GetGlyphInfoSpan(); - + var second = glyphInfos[length - 1]; if (!new Codepoint(second.Codepoint).IsBreakChar) @@ -98,7 +98,7 @@ namespace Avalonia.Skia if (length > 1 && glyphInfos[length - 2].Codepoint == '\r' && second.Codepoint == '\n') { var first = glyphInfos[length - 2]; - + first.Codepoint = '\u200C'; second.Codepoint = '\u200C'; second.Cluster = first.Cluster; @@ -109,7 +109,7 @@ namespace Avalonia.Skia { *p = first; } - + fixed (GlyphInfo* p = &glyphInfos[length - 1]) { *p = second; diff --git a/src/Skia/Avalonia.Skia/WriteableBitmapImpl.cs b/src/Skia/Avalonia.Skia/WriteableBitmapImpl.cs index d437f514bb..dcb267b2a3 100644 --- a/src/Skia/Avalonia.Skia/WriteableBitmapImpl.cs +++ b/src/Skia/Avalonia.Skia/WriteableBitmapImpl.cs @@ -217,8 +217,7 @@ namespace Avalonia.Skia public int RowBytes => _bitmap.RowBytes; /// - public Vector Dpi { get; } = SkiaPlatform.DefaultDpi; - + public Vector Dpi => _parent.Dpi; /// public PixelFormat Format => _bitmap.ColorType.ToPixelFormat(); } diff --git a/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs b/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs index 4d307c9762..a5f77230b7 100644 --- a/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs +++ b/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs @@ -113,7 +113,7 @@ namespace Avalonia.Direct2D1 SharpDX.Configuration.EnableReleaseOnFinalizer = true; } - public IRenderTarget CreateRenderTarget(IEnumerable surfaces) + private IRenderTarget CreateRenderTarget(IEnumerable surfaces) { foreach (var s in surfaces) { @@ -223,6 +223,26 @@ namespace Avalonia.Direct2D1 return new GlyphRunImpl(run); } + class D2DApi : IPlatformRenderInterfaceContext + { + private readonly Direct2D1Platform _platform; + + public D2DApi(Direct2D1Platform platform) + { + _platform = platform; + } + public object TryGetFeature(Type featureType) => null; + + public void Dispose() + { + } + + public IRenderTarget CreateRenderTarget(IEnumerable surfaces) => _platform.CreateRenderTarget(surfaces); + } + + public IPlatformRenderInterfaceContext CreateBackendContext(IPlatformGraphicsContext graphicsContext) => + new D2DApi(this); + public IGeometryImpl BuildGlyphRunGeometry(GlyphRun glyphRun) { if (glyphRun.GlyphTypeface is not GlyphTypefaceImpl glyphTypeface) diff --git a/src/Windows/Avalonia.Direct2D1/ExternalRenderTarget.cs b/src/Windows/Avalonia.Direct2D1/ExternalRenderTarget.cs index 2c0adcac32..02932c52da 100644 --- a/src/Windows/Avalonia.Direct2D1/ExternalRenderTarget.cs +++ b/src/Windows/Avalonia.Direct2D1/ExternalRenderTarget.cs @@ -38,6 +38,8 @@ namespace Avalonia.Direct2D1 }); } + public bool IsCorrupted => false; + public IDrawingContextLayerImpl CreateLayer(Size size) { var renderTarget = _externalRenderTargetProvider.GetOrCreateRenderTarget(); diff --git a/src/Windows/Avalonia.Direct2D1/FramebufferShimRenderTarget.cs b/src/Windows/Avalonia.Direct2D1/FramebufferShimRenderTarget.cs index f7e7ed3dc2..984a24fb30 100644 --- a/src/Windows/Avalonia.Direct2D1/FramebufferShimRenderTarget.cs +++ b/src/Windows/Avalonia.Direct2D1/FramebufferShimRenderTarget.cs @@ -35,6 +35,8 @@ namespace Avalonia.Direct2D1 .CreateDrawingContext(visualBrushRenderer); } + public bool IsCorrupted => false; + class FramebufferShim : WicRenderTargetBitmapImpl { private readonly ILockedFramebuffer _target; diff --git a/src/Windows/Avalonia.Direct2D1/Media/Imaging/D2DRenderTargetBitmapImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/Imaging/D2DRenderTargetBitmapImpl.cs index c5f8e837ce..84f11acdd7 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/Imaging/D2DRenderTargetBitmapImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/Imaging/D2DRenderTargetBitmapImpl.cs @@ -37,6 +37,8 @@ namespace Avalonia.Direct2D1.Media.Imaging return new DrawingContextImpl(visualBrushRenderer, this, _renderTarget, null, () => Version++); } + public bool IsCorrupted => false; + public void Blit(IDrawingContextImpl context) => throw new NotSupportedException(); public bool CanBlit => false; diff --git a/src/Windows/Avalonia.Direct2D1/Media/Imaging/WicRenderTargetBitmapImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/Imaging/WicRenderTargetBitmapImpl.cs index 8c9d01f37d..9f0d48dbc7 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/Imaging/WicRenderTargetBitmapImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/Imaging/WicRenderTargetBitmapImpl.cs @@ -39,6 +39,8 @@ namespace Avalonia.Direct2D1.Media public virtual IDrawingContextImpl CreateDrawingContext(IVisualBrushRenderer visualBrushRenderer) => CreateDrawingContext(visualBrushRenderer, null); + public bool IsCorrupted => false; + public IDrawingContextImpl CreateDrawingContext(IVisualBrushRenderer visualBrushRenderer, Action finishedCallback) { return new DrawingContextImpl(visualBrushRenderer, null, _renderTarget, finishedCallback: () => diff --git a/src/Windows/Avalonia.Direct2D1/Media/Imaging/WriteableWicBitmapImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/Imaging/WriteableWicBitmapImpl.cs index 885be132a4..2e40bdd9d1 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/Imaging/WriteableWicBitmapImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/Imaging/WriteableWicBitmapImpl.cs @@ -52,7 +52,7 @@ namespace Avalonia.Direct2D1.Media.Imaging public IntPtr Address => _lock.Data.DataPointer; public PixelSize Size => _lock.Size.ToAvalonia(); public int RowBytes => _lock.Stride; - public Vector Dpi { get; } = new Vector(96, 96); + public Vector Dpi => _parent.Dpi; public PixelFormat Format => _format; } diff --git a/src/Windows/Avalonia.Direct2D1/Media/TextShaperImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/TextShaperImpl.cs index 7f2cbc6182..6685dd00b9 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/TextShaperImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/TextShaperImpl.cs @@ -3,7 +3,6 @@ using System.Globalization; using Avalonia.Media.TextFormatting; using Avalonia.Media.TextFormatting.Unicode; using Avalonia.Platform; -using Avalonia.Utilities; using HarfBuzzSharp; using Buffer = HarfBuzzSharp.Buffer; using GlyphInfo = HarfBuzzSharp.GlyphInfo; @@ -12,7 +11,7 @@ namespace Avalonia.Direct2D1.Media { internal class TextShaperImpl : ITextShaperImpl { - public ShapedBuffer ShapeText(ReadOnlySlice text, TextShaperOptions options) + public ShapedBuffer ShapeText(CharacterBufferReference characterBufferReference, int length, TextShaperOptions options) { var typeface = options.Typeface; var fontRenderingEmSize = options.FontRenderingEmSize; @@ -21,7 +20,7 @@ namespace Avalonia.Direct2D1.Media using (var buffer = new Buffer()) { - buffer.AddUtf16(text.Buffer.Span, text.BufferOffset, text.Length); + buffer.AddUtf16(characterBufferReference.CharacterBuffer.Span, characterBufferReference.OffsetToFirstChar, length); MergeBreakPair(buffer); @@ -46,7 +45,9 @@ namespace Avalonia.Direct2D1.Media var bufferLength = buffer.Length; - var shapedBuffer = new ShapedBuffer(text, bufferLength, typeface, fontRenderingEmSize, bidiLevel); + var characterBufferRange = new CharacterBufferRange(characterBufferReference, length); + + var shapedBuffer = new ShapedBuffer(characterBufferRange, bufferLength, typeface, fontRenderingEmSize, bidiLevel); var glyphInfos = buffer.GetGlyphInfoSpan(); @@ -64,7 +65,7 @@ namespace Avalonia.Direct2D1.Media var glyphOffset = GetGlyphOffset(glyphPositions, i, textScale); - if (text.Buffer.Span[glyphCluster] == '\t') + if (characterBufferRange[i] == '\t') { glyphIndex = typeface.GetGlyph(' '); diff --git a/src/Windows/Avalonia.Direct2D1/OptionalDispose.cs b/src/Windows/Avalonia.Direct2D1/OptionalDispose.cs index e302e71102..1cdf7661df 100644 --- a/src/Windows/Avalonia.Direct2D1/OptionalDispose.cs +++ b/src/Windows/Avalonia.Direct2D1/OptionalDispose.cs @@ -2,7 +2,7 @@ namespace Avalonia.Direct2D1 { - public readonly struct OptionalDispose : IDisposable where T : IDisposable + public readonly record struct OptionalDispose : IDisposable where T : IDisposable { private readonly bool _dispose; diff --git a/src/Windows/Avalonia.Direct2D1/RenderTarget.cs b/src/Windows/Avalonia.Direct2D1/RenderTarget.cs index d04c616bd9..1a749c1a7f 100644 --- a/src/Windows/Avalonia.Direct2D1/RenderTarget.cs +++ b/src/Windows/Avalonia.Direct2D1/RenderTarget.cs @@ -30,6 +30,8 @@ namespace Avalonia.Direct2D1 return new DrawingContextImpl(visualBrushRenderer, this, _renderTarget); } + public bool IsCorrupted => false; + public IDrawingContextLayerImpl CreateLayer(Size size) { return D2DRenderTargetBitmapImpl.CreateCompatible(_renderTarget, size); diff --git a/src/Windows/Avalonia.Direct2D1/SwapChainRenderTarget.cs b/src/Windows/Avalonia.Direct2D1/SwapChainRenderTarget.cs index f319cfae03..4935e3db48 100644 --- a/src/Windows/Avalonia.Direct2D1/SwapChainRenderTarget.cs +++ b/src/Windows/Avalonia.Direct2D1/SwapChainRenderTarget.cs @@ -35,6 +35,8 @@ namespace Avalonia.Direct2D1 return new DrawingContextImpl(visualBrushRenderer, this, _deviceContext, _swapChain); } + public bool IsCorrupted => false; + public IDrawingContextLayerImpl CreateLayer(Size size) { if (_deviceContext == null) diff --git a/src/Windows/Avalonia.Win32.Interop/Wpf/WpfTopLevelImpl.cs b/src/Windows/Avalonia.Win32.Interop/Wpf/WpfTopLevelImpl.cs index 1dcf45aa4e..16a20c855f 100644 --- a/src/Windows/Avalonia.Win32.Interop/Wpf/WpfTopLevelImpl.cs +++ b/src/Windows/Avalonia.Win32.Interop/Wpf/WpfTopLevelImpl.cs @@ -87,9 +87,11 @@ namespace Avalonia.Win32.Interop.Wpf _ttl.ScalingChanged?.Invoke(_ttl.RenderScaling); } + public IRenderer CreateRenderer(IRenderRoot root) { - return new ImmediateRenderer((Visual)root); + var mgr = new PlatformRenderInterfaceContextManager(null); + return new ImmediateRenderer((Visual)root, () => mgr.CreateRenderTarget(_surfaces), mgr); } public void Dispose() diff --git a/src/Avalonia.OpenGL/AngleOptions.cs b/src/Windows/Avalonia.Win32/AngleOptions.cs similarity index 90% rename from src/Avalonia.OpenGL/AngleOptions.cs rename to src/Windows/Avalonia.Win32/AngleOptions.cs index 0807eb7ab4..076ddd2a5d 100644 --- a/src/Avalonia.OpenGL/AngleOptions.cs +++ b/src/Windows/Avalonia.Win32/AngleOptions.cs @@ -1,6 +1,7 @@ using System.Collections.Generic; +using Avalonia.OpenGL; -namespace Avalonia.OpenGL +namespace Avalonia.Win32 { public class AngleOptions { diff --git a/src/Windows/Avalonia.Win32/Avalonia.Win32.csproj b/src/Windows/Avalonia.Win32/Avalonia.Win32.csproj index e69e0ee219..eb51b7fd07 100644 --- a/src/Windows/Avalonia.Win32/Avalonia.Win32.csproj +++ b/src/Windows/Avalonia.Win32/Avalonia.Win32.csproj @@ -16,11 +16,13 @@ + + + + - - $(NoWarn);CA1416 diff --git a/src/Windows/Avalonia.Win32/DirectX/DirectXEnums.cs b/src/Windows/Avalonia.Win32/DirectX/DirectXEnums.cs index 3a67da530f..a749ed1b45 100644 --- a/src/Windows/Avalonia.Win32/DirectX/DirectXEnums.cs +++ b/src/Windows/Avalonia.Win32/DirectX/DirectXEnums.cs @@ -4,7 +4,7 @@ using System.Linq; using System.Text; using System.Threading.Tasks; -namespace Avalonia.Win32.DxgiSwapchain +namespace Avalonia.Win32.DirectX { internal enum D3D_FEATURE_LEVEL { @@ -136,4 +136,53 @@ namespace Avalonia.Win32.DxgiSwapchain DXGI_ALPHA_MODE_FORCE_DWORD = (unchecked((int)0xffffffff)), } + + internal enum D3D_DRIVER_TYPE + { + D3D_DRIVER_TYPE_UNKNOWN = 0, + D3D_DRIVER_TYPE_HARDWARE, + D3D_DRIVER_TYPE_REFERENCE, + D3D_DRIVER_TYPE_NULL, + D3D_DRIVER_TYPE_SOFTWARE, + D3D_DRIVER_TYPE_WARP + } + + internal enum DXGI_ERROR : uint + { + DXGI_ERROR_ACCESS_DENIED = 0x887A002B, + DXGI_ERROR_ACCESS_LOST = 0x887A0026, + DXGI_ERROR_ALREADY_EXISTS = 0x887A0036, + DXGI_ERROR_CANNOT_PROTECT_CONTENT = 0x887A002A, + DXGI_ERROR_DEVICE_HUNG = 0x887A0006, + DXGI_ERROR_DEVICE_REMOVED = 0x887A0005, + DXGI_ERROR_DEVICE_RESET = 0x887A0007, + DXGI_ERROR_DRIVER_INTERNAL_ERROR = 0x887A0020, + DXGI_ERROR_FRAME_STATISTICS_DISJOINT = 0x887A000B, + DXGI_ERROR_GRAPHICS_VIDPN_SOURCE_IN_USE = 0x887A000C, + DXGI_ERROR_INVALID_CALL = 0x887A0001, + DXGI_ERROR_MORE_DATA = 0x887A0003, + DXGI_ERROR_NAME_ALREADY_EXISTS = 0x887A002C, + DXGI_ERROR_NONEXCLUSIVE = 0x887A0021, + DXGI_ERROR_NOT_CURRENTLY_AVAILABLE = 0x887A0022, + DXGI_ERROR_NOT_FOUND = 0x887A0002, + DXGI_ERROR_REMOTE_CLIENT_DISCONNECTED = 0x887A0023, + DXGI_ERROR_REMOTE_OUTOFMEMORY = 0x887A0024, + DXGI_ERROR_RESTRICT_TO_OUTPUT_STALE = 0x887A0029, + DXGI_ERROR_SDK_COMPONENT_MISSING = 0x887A002D, + DXGI_ERROR_SESSION_DISCONNECTED = 0x887A0028, + DXGI_ERROR_UNSUPPORTED = 0x887A0004, + DXGI_ERROR_WAIT_TIMEOUT = 0x887A0027, + DXGI_ERROR_WAS_STILL_DRAWING = 0x887A000A + } + + internal static class DxgiErrorExtensions + { + public static bool IsDeviceLostError(this DXGI_ERROR error) + { + return error is DXGI_ERROR.DXGI_ERROR_DEVICE_REMOVED + or DXGI_ERROR.DXGI_ERROR_DEVICE_HUNG + or DXGI_ERROR.DXGI_ERROR_DEVICE_RESET + or DXGI_ERROR.DXGI_ERROR_NOT_CURRENTLY_AVAILABLE; + } + } } diff --git a/src/Windows/Avalonia.Win32/DirectX/DirectXStructs.cs b/src/Windows/Avalonia.Win32/DirectX/DirectXStructs.cs index d606b00109..47451831a6 100644 --- a/src/Windows/Avalonia.Win32/DirectX/DirectXStructs.cs +++ b/src/Windows/Avalonia.Win32/DirectX/DirectXStructs.cs @@ -6,8 +6,10 @@ using System.Runtime.InteropServices; using System.Text; using System.Threading.Tasks; using static Avalonia.Win32.Interop.UnmanagedMethods; +// ReSharper disable InconsistentNaming +#pragma warning disable CS0649 -namespace Avalonia.Win32.DxgiSwapchain +namespace Avalonia.Win32.DirectX { #nullable enable public unsafe struct HANDLE @@ -36,1092 +38,13 @@ namespace Avalonia.Win32.DxgiSwapchain public override string ToString() => ((IntPtr)Value).ToString(); } - internal unsafe partial struct MONITORINFOEXW + internal unsafe struct MONITORINFOEXW { internal MONITORINFO Base; internal fixed ushort szDevice[32]; } - - internal unsafe struct DXGI_GAMMA_CONTROL - { - public DXGI_RGB Scale; - - public DXGI_RGB Offset; - - public _GammaCurve_e__FixedBuffer GammaCurve; - - public partial struct _GammaCurve_e__FixedBuffer - { - public DXGI_RGB e0; - public DXGI_RGB e1; - public DXGI_RGB e2; - public DXGI_RGB e3; - public DXGI_RGB e4; - public DXGI_RGB e5; - public DXGI_RGB e6; - public DXGI_RGB e7; - public DXGI_RGB e8; - public DXGI_RGB e9; - public DXGI_RGB e10; - public DXGI_RGB e11; - public DXGI_RGB e12; - public DXGI_RGB e13; - public DXGI_RGB e14; - public DXGI_RGB e15; - public DXGI_RGB e16; - public DXGI_RGB e17; - public DXGI_RGB e18; - public DXGI_RGB e19; - public DXGI_RGB e20; - public DXGI_RGB e21; - public DXGI_RGB e22; - public DXGI_RGB e23; - public DXGI_RGB e24; - public DXGI_RGB e25; - public DXGI_RGB e26; - public DXGI_RGB e27; - public DXGI_RGB e28; - public DXGI_RGB e29; - public DXGI_RGB e30; - public DXGI_RGB e31; - public DXGI_RGB e32; - public DXGI_RGB e33; - public DXGI_RGB e34; - public DXGI_RGB e35; - public DXGI_RGB e36; - public DXGI_RGB e37; - public DXGI_RGB e38; - public DXGI_RGB e39; - public DXGI_RGB e40; - public DXGI_RGB e41; - public DXGI_RGB e42; - public DXGI_RGB e43; - public DXGI_RGB e44; - public DXGI_RGB e45; - public DXGI_RGB e46; - public DXGI_RGB e47; - public DXGI_RGB e48; - public DXGI_RGB e49; - public DXGI_RGB e50; - public DXGI_RGB e51; - public DXGI_RGB e52; - public DXGI_RGB e53; - public DXGI_RGB e54; - public DXGI_RGB e55; - public DXGI_RGB e56; - public DXGI_RGB e57; - public DXGI_RGB e58; - public DXGI_RGB e59; - public DXGI_RGB e60; - public DXGI_RGB e61; - public DXGI_RGB e62; - public DXGI_RGB e63; - public DXGI_RGB e64; - public DXGI_RGB e65; - public DXGI_RGB e66; - public DXGI_RGB e67; - public DXGI_RGB e68; - public DXGI_RGB e69; - public DXGI_RGB e70; - public DXGI_RGB e71; - public DXGI_RGB e72; - public DXGI_RGB e73; - public DXGI_RGB e74; - public DXGI_RGB e75; - public DXGI_RGB e76; - public DXGI_RGB e77; - public DXGI_RGB e78; - public DXGI_RGB e79; - public DXGI_RGB e80; - public DXGI_RGB e81; - public DXGI_RGB e82; - public DXGI_RGB e83; - public DXGI_RGB e84; - public DXGI_RGB e85; - public DXGI_RGB e86; - public DXGI_RGB e87; - public DXGI_RGB e88; - public DXGI_RGB e89; - public DXGI_RGB e90; - public DXGI_RGB e91; - public DXGI_RGB e92; - public DXGI_RGB e93; - public DXGI_RGB e94; - public DXGI_RGB e95; - public DXGI_RGB e96; - public DXGI_RGB e97; - public DXGI_RGB e98; - public DXGI_RGB e99; - public DXGI_RGB e100; - public DXGI_RGB e101; - public DXGI_RGB e102; - public DXGI_RGB e103; - public DXGI_RGB e104; - public DXGI_RGB e105; - public DXGI_RGB e106; - public DXGI_RGB e107; - public DXGI_RGB e108; - public DXGI_RGB e109; - public DXGI_RGB e110; - public DXGI_RGB e111; - public DXGI_RGB e112; - public DXGI_RGB e113; - public DXGI_RGB e114; - public DXGI_RGB e115; - public DXGI_RGB e116; - public DXGI_RGB e117; - public DXGI_RGB e118; - public DXGI_RGB e119; - public DXGI_RGB e120; - public DXGI_RGB e121; - public DXGI_RGB e122; - public DXGI_RGB e123; - public DXGI_RGB e124; - public DXGI_RGB e125; - public DXGI_RGB e126; - public DXGI_RGB e127; - public DXGI_RGB e128; - public DXGI_RGB e129; - public DXGI_RGB e130; - public DXGI_RGB e131; - public DXGI_RGB e132; - public DXGI_RGB e133; - public DXGI_RGB e134; - public DXGI_RGB e135; - public DXGI_RGB e136; - public DXGI_RGB e137; - public DXGI_RGB e138; - public DXGI_RGB e139; - public DXGI_RGB e140; - public DXGI_RGB e141; - public DXGI_RGB e142; - public DXGI_RGB e143; - public DXGI_RGB e144; - public DXGI_RGB e145; - public DXGI_RGB e146; - public DXGI_RGB e147; - public DXGI_RGB e148; - public DXGI_RGB e149; - public DXGI_RGB e150; - public DXGI_RGB e151; - public DXGI_RGB e152; - public DXGI_RGB e153; - public DXGI_RGB e154; - public DXGI_RGB e155; - public DXGI_RGB e156; - public DXGI_RGB e157; - public DXGI_RGB e158; - public DXGI_RGB e159; - public DXGI_RGB e160; - public DXGI_RGB e161; - public DXGI_RGB e162; - public DXGI_RGB e163; - public DXGI_RGB e164; - public DXGI_RGB e165; - public DXGI_RGB e166; - public DXGI_RGB e167; - public DXGI_RGB e168; - public DXGI_RGB e169; - public DXGI_RGB e170; - public DXGI_RGB e171; - public DXGI_RGB e172; - public DXGI_RGB e173; - public DXGI_RGB e174; - public DXGI_RGB e175; - public DXGI_RGB e176; - public DXGI_RGB e177; - public DXGI_RGB e178; - public DXGI_RGB e179; - public DXGI_RGB e180; - public DXGI_RGB e181; - public DXGI_RGB e182; - public DXGI_RGB e183; - public DXGI_RGB e184; - public DXGI_RGB e185; - public DXGI_RGB e186; - public DXGI_RGB e187; - public DXGI_RGB e188; - public DXGI_RGB e189; - public DXGI_RGB e190; - public DXGI_RGB e191; - public DXGI_RGB e192; - public DXGI_RGB e193; - public DXGI_RGB e194; - public DXGI_RGB e195; - public DXGI_RGB e196; - public DXGI_RGB e197; - public DXGI_RGB e198; - public DXGI_RGB e199; - public DXGI_RGB e200; - public DXGI_RGB e201; - public DXGI_RGB e202; - public DXGI_RGB e203; - public DXGI_RGB e204; - public DXGI_RGB e205; - public DXGI_RGB e206; - public DXGI_RGB e207; - public DXGI_RGB e208; - public DXGI_RGB e209; - public DXGI_RGB e210; - public DXGI_RGB e211; - public DXGI_RGB e212; - public DXGI_RGB e213; - public DXGI_RGB e214; - public DXGI_RGB e215; - public DXGI_RGB e216; - public DXGI_RGB e217; - public DXGI_RGB e218; - public DXGI_RGB e219; - public DXGI_RGB e220; - public DXGI_RGB e221; - public DXGI_RGB e222; - public DXGI_RGB e223; - public DXGI_RGB e224; - public DXGI_RGB e225; - public DXGI_RGB e226; - public DXGI_RGB e227; - public DXGI_RGB e228; - public DXGI_RGB e229; - public DXGI_RGB e230; - public DXGI_RGB e231; - public DXGI_RGB e232; - public DXGI_RGB e233; - public DXGI_RGB e234; - public DXGI_RGB e235; - public DXGI_RGB e236; - public DXGI_RGB e237; - public DXGI_RGB e238; - public DXGI_RGB e239; - public DXGI_RGB e240; - public DXGI_RGB e241; - public DXGI_RGB e242; - public DXGI_RGB e243; - public DXGI_RGB e244; - public DXGI_RGB e245; - public DXGI_RGB e246; - public DXGI_RGB e247; - public DXGI_RGB e248; - public DXGI_RGB e249; - public DXGI_RGB e250; - public DXGI_RGB e251; - public DXGI_RGB e252; - public DXGI_RGB e253; - public DXGI_RGB e254; - public DXGI_RGB e255; - public DXGI_RGB e256; - public DXGI_RGB e257; - public DXGI_RGB e258; - public DXGI_RGB e259; - public DXGI_RGB e260; - public DXGI_RGB e261; - public DXGI_RGB e262; - public DXGI_RGB e263; - public DXGI_RGB e264; - public DXGI_RGB e265; - public DXGI_RGB e266; - public DXGI_RGB e267; - public DXGI_RGB e268; - public DXGI_RGB e269; - public DXGI_RGB e270; - public DXGI_RGB e271; - public DXGI_RGB e272; - public DXGI_RGB e273; - public DXGI_RGB e274; - public DXGI_RGB e275; - public DXGI_RGB e276; - public DXGI_RGB e277; - public DXGI_RGB e278; - public DXGI_RGB e279; - public DXGI_RGB e280; - public DXGI_RGB e281; - public DXGI_RGB e282; - public DXGI_RGB e283; - public DXGI_RGB e284; - public DXGI_RGB e285; - public DXGI_RGB e286; - public DXGI_RGB e287; - public DXGI_RGB e288; - public DXGI_RGB e289; - public DXGI_RGB e290; - public DXGI_RGB e291; - public DXGI_RGB e292; - public DXGI_RGB e293; - public DXGI_RGB e294; - public DXGI_RGB e295; - public DXGI_RGB e296; - public DXGI_RGB e297; - public DXGI_RGB e298; - public DXGI_RGB e299; - public DXGI_RGB e300; - public DXGI_RGB e301; - public DXGI_RGB e302; - public DXGI_RGB e303; - public DXGI_RGB e304; - public DXGI_RGB e305; - public DXGI_RGB e306; - public DXGI_RGB e307; - public DXGI_RGB e308; - public DXGI_RGB e309; - public DXGI_RGB e310; - public DXGI_RGB e311; - public DXGI_RGB e312; - public DXGI_RGB e313; - public DXGI_RGB e314; - public DXGI_RGB e315; - public DXGI_RGB e316; - public DXGI_RGB e317; - public DXGI_RGB e318; - public DXGI_RGB e319; - public DXGI_RGB e320; - public DXGI_RGB e321; - public DXGI_RGB e322; - public DXGI_RGB e323; - public DXGI_RGB e324; - public DXGI_RGB e325; - public DXGI_RGB e326; - public DXGI_RGB e327; - public DXGI_RGB e328; - public DXGI_RGB e329; - public DXGI_RGB e330; - public DXGI_RGB e331; - public DXGI_RGB e332; - public DXGI_RGB e333; - public DXGI_RGB e334; - public DXGI_RGB e335; - public DXGI_RGB e336; - public DXGI_RGB e337; - public DXGI_RGB e338; - public DXGI_RGB e339; - public DXGI_RGB e340; - public DXGI_RGB e341; - public DXGI_RGB e342; - public DXGI_RGB e343; - public DXGI_RGB e344; - public DXGI_RGB e345; - public DXGI_RGB e346; - public DXGI_RGB e347; - public DXGI_RGB e348; - public DXGI_RGB e349; - public DXGI_RGB e350; - public DXGI_RGB e351; - public DXGI_RGB e352; - public DXGI_RGB e353; - public DXGI_RGB e354; - public DXGI_RGB e355; - public DXGI_RGB e356; - public DXGI_RGB e357; - public DXGI_RGB e358; - public DXGI_RGB e359; - public DXGI_RGB e360; - public DXGI_RGB e361; - public DXGI_RGB e362; - public DXGI_RGB e363; - public DXGI_RGB e364; - public DXGI_RGB e365; - public DXGI_RGB e366; - public DXGI_RGB e367; - public DXGI_RGB e368; - public DXGI_RGB e369; - public DXGI_RGB e370; - public DXGI_RGB e371; - public DXGI_RGB e372; - public DXGI_RGB e373; - public DXGI_RGB e374; - public DXGI_RGB e375; - public DXGI_RGB e376; - public DXGI_RGB e377; - public DXGI_RGB e378; - public DXGI_RGB e379; - public DXGI_RGB e380; - public DXGI_RGB e381; - public DXGI_RGB e382; - public DXGI_RGB e383; - public DXGI_RGB e384; - public DXGI_RGB e385; - public DXGI_RGB e386; - public DXGI_RGB e387; - public DXGI_RGB e388; - public DXGI_RGB e389; - public DXGI_RGB e390; - public DXGI_RGB e391; - public DXGI_RGB e392; - public DXGI_RGB e393; - public DXGI_RGB e394; - public DXGI_RGB e395; - public DXGI_RGB e396; - public DXGI_RGB e397; - public DXGI_RGB e398; - public DXGI_RGB e399; - public DXGI_RGB e400; - public DXGI_RGB e401; - public DXGI_RGB e402; - public DXGI_RGB e403; - public DXGI_RGB e404; - public DXGI_RGB e405; - public DXGI_RGB e406; - public DXGI_RGB e407; - public DXGI_RGB e408; - public DXGI_RGB e409; - public DXGI_RGB e410; - public DXGI_RGB e411; - public DXGI_RGB e412; - public DXGI_RGB e413; - public DXGI_RGB e414; - public DXGI_RGB e415; - public DXGI_RGB e416; - public DXGI_RGB e417; - public DXGI_RGB e418; - public DXGI_RGB e419; - public DXGI_RGB e420; - public DXGI_RGB e421; - public DXGI_RGB e422; - public DXGI_RGB e423; - public DXGI_RGB e424; - public DXGI_RGB e425; - public DXGI_RGB e426; - public DXGI_RGB e427; - public DXGI_RGB e428; - public DXGI_RGB e429; - public DXGI_RGB e430; - public DXGI_RGB e431; - public DXGI_RGB e432; - public DXGI_RGB e433; - public DXGI_RGB e434; - public DXGI_RGB e435; - public DXGI_RGB e436; - public DXGI_RGB e437; - public DXGI_RGB e438; - public DXGI_RGB e439; - public DXGI_RGB e440; - public DXGI_RGB e441; - public DXGI_RGB e442; - public DXGI_RGB e443; - public DXGI_RGB e444; - public DXGI_RGB e445; - public DXGI_RGB e446; - public DXGI_RGB e447; - public DXGI_RGB e448; - public DXGI_RGB e449; - public DXGI_RGB e450; - public DXGI_RGB e451; - public DXGI_RGB e452; - public DXGI_RGB e453; - public DXGI_RGB e454; - public DXGI_RGB e455; - public DXGI_RGB e456; - public DXGI_RGB e457; - public DXGI_RGB e458; - public DXGI_RGB e459; - public DXGI_RGB e460; - public DXGI_RGB e461; - public DXGI_RGB e462; - public DXGI_RGB e463; - public DXGI_RGB e464; - public DXGI_RGB e465; - public DXGI_RGB e466; - public DXGI_RGB e467; - public DXGI_RGB e468; - public DXGI_RGB e469; - public DXGI_RGB e470; - public DXGI_RGB e471; - public DXGI_RGB e472; - public DXGI_RGB e473; - public DXGI_RGB e474; - public DXGI_RGB e475; - public DXGI_RGB e476; - public DXGI_RGB e477; - public DXGI_RGB e478; - public DXGI_RGB e479; - public DXGI_RGB e480; - public DXGI_RGB e481; - public DXGI_RGB e482; - public DXGI_RGB e483; - public DXGI_RGB e484; - public DXGI_RGB e485; - public DXGI_RGB e486; - public DXGI_RGB e487; - public DXGI_RGB e488; - public DXGI_RGB e489; - public DXGI_RGB e490; - public DXGI_RGB e491; - public DXGI_RGB e492; - public DXGI_RGB e493; - public DXGI_RGB e494; - public DXGI_RGB e495; - public DXGI_RGB e496; - public DXGI_RGB e497; - public DXGI_RGB e498; - public DXGI_RGB e499; - public DXGI_RGB e500; - public DXGI_RGB e501; - public DXGI_RGB e502; - public DXGI_RGB e503; - public DXGI_RGB e504; - public DXGI_RGB e505; - public DXGI_RGB e506; - public DXGI_RGB e507; - public DXGI_RGB e508; - public DXGI_RGB e509; - public DXGI_RGB e510; - public DXGI_RGB e511; - public DXGI_RGB e512; - public DXGI_RGB e513; - public DXGI_RGB e514; - public DXGI_RGB e515; - public DXGI_RGB e516; - public DXGI_RGB e517; - public DXGI_RGB e518; - public DXGI_RGB e519; - public DXGI_RGB e520; - public DXGI_RGB e521; - public DXGI_RGB e522; - public DXGI_RGB e523; - public DXGI_RGB e524; - public DXGI_RGB e525; - public DXGI_RGB e526; - public DXGI_RGB e527; - public DXGI_RGB e528; - public DXGI_RGB e529; - public DXGI_RGB e530; - public DXGI_RGB e531; - public DXGI_RGB e532; - public DXGI_RGB e533; - public DXGI_RGB e534; - public DXGI_RGB e535; - public DXGI_RGB e536; - public DXGI_RGB e537; - public DXGI_RGB e538; - public DXGI_RGB e539; - public DXGI_RGB e540; - public DXGI_RGB e541; - public DXGI_RGB e542; - public DXGI_RGB e543; - public DXGI_RGB e544; - public DXGI_RGB e545; - public DXGI_RGB e546; - public DXGI_RGB e547; - public DXGI_RGB e548; - public DXGI_RGB e549; - public DXGI_RGB e550; - public DXGI_RGB e551; - public DXGI_RGB e552; - public DXGI_RGB e553; - public DXGI_RGB e554; - public DXGI_RGB e555; - public DXGI_RGB e556; - public DXGI_RGB e557; - public DXGI_RGB e558; - public DXGI_RGB e559; - public DXGI_RGB e560; - public DXGI_RGB e561; - public DXGI_RGB e562; - public DXGI_RGB e563; - public DXGI_RGB e564; - public DXGI_RGB e565; - public DXGI_RGB e566; - public DXGI_RGB e567; - public DXGI_RGB e568; - public DXGI_RGB e569; - public DXGI_RGB e570; - public DXGI_RGB e571; - public DXGI_RGB e572; - public DXGI_RGB e573; - public DXGI_RGB e574; - public DXGI_RGB e575; - public DXGI_RGB e576; - public DXGI_RGB e577; - public DXGI_RGB e578; - public DXGI_RGB e579; - public DXGI_RGB e580; - public DXGI_RGB e581; - public DXGI_RGB e582; - public DXGI_RGB e583; - public DXGI_RGB e584; - public DXGI_RGB e585; - public DXGI_RGB e586; - public DXGI_RGB e587; - public DXGI_RGB e588; - public DXGI_RGB e589; - public DXGI_RGB e590; - public DXGI_RGB e591; - public DXGI_RGB e592; - public DXGI_RGB e593; - public DXGI_RGB e594; - public DXGI_RGB e595; - public DXGI_RGB e596; - public DXGI_RGB e597; - public DXGI_RGB e598; - public DXGI_RGB e599; - public DXGI_RGB e600; - public DXGI_RGB e601; - public DXGI_RGB e602; - public DXGI_RGB e603; - public DXGI_RGB e604; - public DXGI_RGB e605; - public DXGI_RGB e606; - public DXGI_RGB e607; - public DXGI_RGB e608; - public DXGI_RGB e609; - public DXGI_RGB e610; - public DXGI_RGB e611; - public DXGI_RGB e612; - public DXGI_RGB e613; - public DXGI_RGB e614; - public DXGI_RGB e615; - public DXGI_RGB e616; - public DXGI_RGB e617; - public DXGI_RGB e618; - public DXGI_RGB e619; - public DXGI_RGB e620; - public DXGI_RGB e621; - public DXGI_RGB e622; - public DXGI_RGB e623; - public DXGI_RGB e624; - public DXGI_RGB e625; - public DXGI_RGB e626; - public DXGI_RGB e627; - public DXGI_RGB e628; - public DXGI_RGB e629; - public DXGI_RGB e630; - public DXGI_RGB e631; - public DXGI_RGB e632; - public DXGI_RGB e633; - public DXGI_RGB e634; - public DXGI_RGB e635; - public DXGI_RGB e636; - public DXGI_RGB e637; - public DXGI_RGB e638; - public DXGI_RGB e639; - public DXGI_RGB e640; - public DXGI_RGB e641; - public DXGI_RGB e642; - public DXGI_RGB e643; - public DXGI_RGB e644; - public DXGI_RGB e645; - public DXGI_RGB e646; - public DXGI_RGB e647; - public DXGI_RGB e648; - public DXGI_RGB e649; - public DXGI_RGB e650; - public DXGI_RGB e651; - public DXGI_RGB e652; - public DXGI_RGB e653; - public DXGI_RGB e654; - public DXGI_RGB e655; - public DXGI_RGB e656; - public DXGI_RGB e657; - public DXGI_RGB e658; - public DXGI_RGB e659; - public DXGI_RGB e660; - public DXGI_RGB e661; - public DXGI_RGB e662; - public DXGI_RGB e663; - public DXGI_RGB e664; - public DXGI_RGB e665; - public DXGI_RGB e666; - public DXGI_RGB e667; - public DXGI_RGB e668; - public DXGI_RGB e669; - public DXGI_RGB e670; - public DXGI_RGB e671; - public DXGI_RGB e672; - public DXGI_RGB e673; - public DXGI_RGB e674; - public DXGI_RGB e675; - public DXGI_RGB e676; - public DXGI_RGB e677; - public DXGI_RGB e678; - public DXGI_RGB e679; - public DXGI_RGB e680; - public DXGI_RGB e681; - public DXGI_RGB e682; - public DXGI_RGB e683; - public DXGI_RGB e684; - public DXGI_RGB e685; - public DXGI_RGB e686; - public DXGI_RGB e687; - public DXGI_RGB e688; - public DXGI_RGB e689; - public DXGI_RGB e690; - public DXGI_RGB e691; - public DXGI_RGB e692; - public DXGI_RGB e693; - public DXGI_RGB e694; - public DXGI_RGB e695; - public DXGI_RGB e696; - public DXGI_RGB e697; - public DXGI_RGB e698; - public DXGI_RGB e699; - public DXGI_RGB e700; - public DXGI_RGB e701; - public DXGI_RGB e702; - public DXGI_RGB e703; - public DXGI_RGB e704; - public DXGI_RGB e705; - public DXGI_RGB e706; - public DXGI_RGB e707; - public DXGI_RGB e708; - public DXGI_RGB e709; - public DXGI_RGB e710; - public DXGI_RGB e711; - public DXGI_RGB e712; - public DXGI_RGB e713; - public DXGI_RGB e714; - public DXGI_RGB e715; - public DXGI_RGB e716; - public DXGI_RGB e717; - public DXGI_RGB e718; - public DXGI_RGB e719; - public DXGI_RGB e720; - public DXGI_RGB e721; - public DXGI_RGB e722; - public DXGI_RGB e723; - public DXGI_RGB e724; - public DXGI_RGB e725; - public DXGI_RGB e726; - public DXGI_RGB e727; - public DXGI_RGB e728; - public DXGI_RGB e729; - public DXGI_RGB e730; - public DXGI_RGB e731; - public DXGI_RGB e732; - public DXGI_RGB e733; - public DXGI_RGB e734; - public DXGI_RGB e735; - public DXGI_RGB e736; - public DXGI_RGB e737; - public DXGI_RGB e738; - public DXGI_RGB e739; - public DXGI_RGB e740; - public DXGI_RGB e741; - public DXGI_RGB e742; - public DXGI_RGB e743; - public DXGI_RGB e744; - public DXGI_RGB e745; - public DXGI_RGB e746; - public DXGI_RGB e747; - public DXGI_RGB e748; - public DXGI_RGB e749; - public DXGI_RGB e750; - public DXGI_RGB e751; - public DXGI_RGB e752; - public DXGI_RGB e753; - public DXGI_RGB e754; - public DXGI_RGB e755; - public DXGI_RGB e756; - public DXGI_RGB e757; - public DXGI_RGB e758; - public DXGI_RGB e759; - public DXGI_RGB e760; - public DXGI_RGB e761; - public DXGI_RGB e762; - public DXGI_RGB e763; - public DXGI_RGB e764; - public DXGI_RGB e765; - public DXGI_RGB e766; - public DXGI_RGB e767; - public DXGI_RGB e768; - public DXGI_RGB e769; - public DXGI_RGB e770; - public DXGI_RGB e771; - public DXGI_RGB e772; - public DXGI_RGB e773; - public DXGI_RGB e774; - public DXGI_RGB e775; - public DXGI_RGB e776; - public DXGI_RGB e777; - public DXGI_RGB e778; - public DXGI_RGB e779; - public DXGI_RGB e780; - public DXGI_RGB e781; - public DXGI_RGB e782; - public DXGI_RGB e783; - public DXGI_RGB e784; - public DXGI_RGB e785; - public DXGI_RGB e786; - public DXGI_RGB e787; - public DXGI_RGB e788; - public DXGI_RGB e789; - public DXGI_RGB e790; - public DXGI_RGB e791; - public DXGI_RGB e792; - public DXGI_RGB e793; - public DXGI_RGB e794; - public DXGI_RGB e795; - public DXGI_RGB e796; - public DXGI_RGB e797; - public DXGI_RGB e798; - public DXGI_RGB e799; - public DXGI_RGB e800; - public DXGI_RGB e801; - public DXGI_RGB e802; - public DXGI_RGB e803; - public DXGI_RGB e804; - public DXGI_RGB e805; - public DXGI_RGB e806; - public DXGI_RGB e807; - public DXGI_RGB e808; - public DXGI_RGB e809; - public DXGI_RGB e810; - public DXGI_RGB e811; - public DXGI_RGB e812; - public DXGI_RGB e813; - public DXGI_RGB e814; - public DXGI_RGB e815; - public DXGI_RGB e816; - public DXGI_RGB e817; - public DXGI_RGB e818; - public DXGI_RGB e819; - public DXGI_RGB e820; - public DXGI_RGB e821; - public DXGI_RGB e822; - public DXGI_RGB e823; - public DXGI_RGB e824; - public DXGI_RGB e825; - public DXGI_RGB e826; - public DXGI_RGB e827; - public DXGI_RGB e828; - public DXGI_RGB e829; - public DXGI_RGB e830; - public DXGI_RGB e831; - public DXGI_RGB e832; - public DXGI_RGB e833; - public DXGI_RGB e834; - public DXGI_RGB e835; - public DXGI_RGB e836; - public DXGI_RGB e837; - public DXGI_RGB e838; - public DXGI_RGB e839; - public DXGI_RGB e840; - public DXGI_RGB e841; - public DXGI_RGB e842; - public DXGI_RGB e843; - public DXGI_RGB e844; - public DXGI_RGB e845; - public DXGI_RGB e846; - public DXGI_RGB e847; - public DXGI_RGB e848; - public DXGI_RGB e849; - public DXGI_RGB e850; - public DXGI_RGB e851; - public DXGI_RGB e852; - public DXGI_RGB e853; - public DXGI_RGB e854; - public DXGI_RGB e855; - public DXGI_RGB e856; - public DXGI_RGB e857; - public DXGI_RGB e858; - public DXGI_RGB e859; - public DXGI_RGB e860; - public DXGI_RGB e861; - public DXGI_RGB e862; - public DXGI_RGB e863; - public DXGI_RGB e864; - public DXGI_RGB e865; - public DXGI_RGB e866; - public DXGI_RGB e867; - public DXGI_RGB e868; - public DXGI_RGB e869; - public DXGI_RGB e870; - public DXGI_RGB e871; - public DXGI_RGB e872; - public DXGI_RGB e873; - public DXGI_RGB e874; - public DXGI_RGB e875; - public DXGI_RGB e876; - public DXGI_RGB e877; - public DXGI_RGB e878; - public DXGI_RGB e879; - public DXGI_RGB e880; - public DXGI_RGB e881; - public DXGI_RGB e882; - public DXGI_RGB e883; - public DXGI_RGB e884; - public DXGI_RGB e885; - public DXGI_RGB e886; - public DXGI_RGB e887; - public DXGI_RGB e888; - public DXGI_RGB e889; - public DXGI_RGB e890; - public DXGI_RGB e891; - public DXGI_RGB e892; - public DXGI_RGB e893; - public DXGI_RGB e894; - public DXGI_RGB e895; - public DXGI_RGB e896; - public DXGI_RGB e897; - public DXGI_RGB e898; - public DXGI_RGB e899; - public DXGI_RGB e900; - public DXGI_RGB e901; - public DXGI_RGB e902; - public DXGI_RGB e903; - public DXGI_RGB e904; - public DXGI_RGB e905; - public DXGI_RGB e906; - public DXGI_RGB e907; - public DXGI_RGB e908; - public DXGI_RGB e909; - public DXGI_RGB e910; - public DXGI_RGB e911; - public DXGI_RGB e912; - public DXGI_RGB e913; - public DXGI_RGB e914; - public DXGI_RGB e915; - public DXGI_RGB e916; - public DXGI_RGB e917; - public DXGI_RGB e918; - public DXGI_RGB e919; - public DXGI_RGB e920; - public DXGI_RGB e921; - public DXGI_RGB e922; - public DXGI_RGB e923; - public DXGI_RGB e924; - public DXGI_RGB e925; - public DXGI_RGB e926; - public DXGI_RGB e927; - public DXGI_RGB e928; - public DXGI_RGB e929; - public DXGI_RGB e930; - public DXGI_RGB e931; - public DXGI_RGB e932; - public DXGI_RGB e933; - public DXGI_RGB e934; - public DXGI_RGB e935; - public DXGI_RGB e936; - public DXGI_RGB e937; - public DXGI_RGB e938; - public DXGI_RGB e939; - public DXGI_RGB e940; - public DXGI_RGB e941; - public DXGI_RGB e942; - public DXGI_RGB e943; - public DXGI_RGB e944; - public DXGI_RGB e945; - public DXGI_RGB e946; - public DXGI_RGB e947; - public DXGI_RGB e948; - public DXGI_RGB e949; - public DXGI_RGB e950; - public DXGI_RGB e951; - public DXGI_RGB e952; - public DXGI_RGB e953; - public DXGI_RGB e954; - public DXGI_RGB e955; - public DXGI_RGB e956; - public DXGI_RGB e957; - public DXGI_RGB e958; - public DXGI_RGB e959; - public DXGI_RGB e960; - public DXGI_RGB e961; - public DXGI_RGB e962; - public DXGI_RGB e963; - public DXGI_RGB e964; - public DXGI_RGB e965; - public DXGI_RGB e966; - public DXGI_RGB e967; - public DXGI_RGB e968; - public DXGI_RGB e969; - public DXGI_RGB e970; - public DXGI_RGB e971; - public DXGI_RGB e972; - public DXGI_RGB e973; - public DXGI_RGB e974; - public DXGI_RGB e975; - public DXGI_RGB e976; - public DXGI_RGB e977; - public DXGI_RGB e978; - public DXGI_RGB e979; - public DXGI_RGB e980; - public DXGI_RGB e981; - public DXGI_RGB e982; - public DXGI_RGB e983; - public DXGI_RGB e984; - public DXGI_RGB e985; - public DXGI_RGB e986; - public DXGI_RGB e987; - public DXGI_RGB e988; - public DXGI_RGB e989; - public DXGI_RGB e990; - public DXGI_RGB e991; - public DXGI_RGB e992; - public DXGI_RGB e993; - public DXGI_RGB e994; - public DXGI_RGB e995; - public DXGI_RGB e996; - public DXGI_RGB e997; - public DXGI_RGB e998; - public DXGI_RGB e999; - public DXGI_RGB e1000; - public DXGI_RGB e1001; - public DXGI_RGB e1002; - public DXGI_RGB e1003; - public DXGI_RGB e1004; - public DXGI_RGB e1005; - public DXGI_RGB e1006; - public DXGI_RGB e1007; - public DXGI_RGB e1008; - public DXGI_RGB e1009; - public DXGI_RGB e1010; - public DXGI_RGB e1011; - public DXGI_RGB e1012; - public DXGI_RGB e1013; - public DXGI_RGB e1014; - public DXGI_RGB e1015; - public DXGI_RGB e1016; - public DXGI_RGB e1017; - public DXGI_RGB e1018; - public DXGI_RGB e1019; - public DXGI_RGB e1020; - public DXGI_RGB e1021; - public DXGI_RGB e1022; - public DXGI_RGB e1023; - public DXGI_RGB e1024; -#if NET6_0_OR_GREATER - public ref DXGI_RGB this[int index] - { - [MethodImpl(MethodImplOptions.AggressiveInlining)] - get - { - return ref AsSpan()[index]; - } - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public Span AsSpan() => MemoryMarshal.CreateSpan(ref e0, 1025); -#else - // there is no way to do this outside of terrible unsafe code. Don't do this in .Net Standard 2.0 - - public DXGI_RGB this[int index] - { - get - { - if ((uint)index > 1025) - throw new ArgumentOutOfRangeException("index"); - - fixed (DXGI_RGB* basePtr = &e0) - { - DXGI_RGB* newPtr = basePtr + index; - return *newPtr; - } - } - set - { - if ((uint)index > 1025) - throw new ArgumentOutOfRangeException("index"); - - fixed (DXGI_RGB* basePtr = &e0) - { - DXGI_RGB* newPtr = basePtr + index; - *newPtr = value; - } - } - } -#endif - } - } - + internal unsafe struct DEVMODEW { public fixed ushort dmDeviceName[32]; @@ -1196,7 +119,7 @@ namespace Avalonia.Win32.DxgiSwapchain public uint Flags; } - internal unsafe struct DXGI_FRAME_STATISTICS + internal struct DXGI_FRAME_STATISTICS { public uint PresentCount; @@ -1225,11 +148,10 @@ namespace Avalonia.Win32.DxgiSwapchain internal unsafe struct DXGI_MAPPED_RECT { public int Pitch; - public byte* pBits; } - internal unsafe partial struct DXGI_MODE_DESC + internal struct DXGI_MODE_DESC { public ushort Width; public ushort Height; @@ -1239,7 +161,7 @@ namespace Avalonia.Win32.DxgiSwapchain public DXGI_MODE_SCALING Scaling; } - internal unsafe partial struct DXGI_OUTPUT_DESC + internal unsafe struct DXGI_OUTPUT_DESC { internal fixed ushort DeviceName[32]; @@ -1263,13 +185,13 @@ namespace Avalonia.Win32.DxgiSwapchain public POINT* pScrollOffset; } - internal unsafe partial struct DXGI_RATIONAL + internal struct DXGI_RATIONAL { public ushort Numerator; public ushort Denominator; } - internal partial struct DXGI_RGB + internal struct DXGI_RGB { public float Red; @@ -1278,7 +200,7 @@ namespace Avalonia.Win32.DxgiSwapchain public float Blue; } - internal partial struct DXGI_RGBA + internal struct DXGI_RGBA { public float r; @@ -1295,7 +217,7 @@ namespace Avalonia.Win32.DxgiSwapchain public uint Quality; } - internal unsafe struct DXGI_SURFACE_DESC + internal struct DXGI_SURFACE_DESC { public uint Width; @@ -1306,7 +228,7 @@ namespace Avalonia.Win32.DxgiSwapchain public DXGI_SAMPLE_DESC SampleDesc; } - internal unsafe partial struct DXGI_SWAP_CHAIN_DESC + internal struct DXGI_SWAP_CHAIN_DESC { public DXGI_MODE_DESC BufferDesc; public DXGI_SAMPLE_DESC SampleDesc; @@ -1333,7 +255,7 @@ namespace Avalonia.Win32.DxgiSwapchain public uint Flags; } - internal unsafe struct DXGI_SWAP_CHAIN_FULLSCREEN_DESC + internal struct DXGI_SWAP_CHAIN_FULLSCREEN_DESC { public DXGI_RATIONAL RefreshRate; @@ -1344,7 +266,7 @@ namespace Avalonia.Win32.DxgiSwapchain public int Windowed; } - internal partial struct D3D11_TEXTURE2D_DESC + internal struct D3D11_TEXTURE2D_DESC { public uint Width; diff --git a/src/Windows/Avalonia.Win32/DirectX/DirectXUnmanagedMethods.cs b/src/Windows/Avalonia.Win32/DirectX/DirectXUnmanagedMethods.cs index 1a739ce3d1..14a9ff1277 100644 --- a/src/Windows/Avalonia.Win32/DirectX/DirectXUnmanagedMethods.cs +++ b/src/Windows/Avalonia.Win32/DirectX/DirectXUnmanagedMethods.cs @@ -5,7 +5,7 @@ using System.Runtime.InteropServices; using System.Text; using System.Threading.Tasks; -namespace Avalonia.Win32.DxgiSwapchain +namespace Avalonia.Win32.DirectX { internal unsafe class DirectXUnmanagedMethods { @@ -23,7 +23,16 @@ namespace Avalonia.Win32.DxgiSwapchain [DllImport("user32", ExactSpelling = true)] internal static extern bool EnumDisplaySettingsW(ushort* lpszDeviceName, uint iModeNum, DEVMODEW* lpDevMode); - [DllImport("user32", ExactSpelling = true, SetLastError = true)] - internal static extern bool GetClientRect(IntPtr hWnd, Interop.UnmanagedMethods.RECT* lpRect); + [DllImport("d3d11", ExactSpelling = true, PreserveSig = false)] + public static extern void D3D11CreateDevice( + IntPtr adapter, D3D_DRIVER_TYPE DriverType, + IntPtr Software, + uint Flags, + D3D_FEATURE_LEVEL[] pFeatureLevels, + uint FeatureLevels, + uint SDKVersion, + out IntPtr ppDevice, + out D3D_FEATURE_LEVEL pFeatureLevel, + IntPtr* ppImmediateContext); } } diff --git a/src/Windows/Avalonia.Win32/DirectX/DxgiConnection.cs b/src/Windows/Avalonia.Win32/DirectX/DxgiConnection.cs index 8c13ecdcc1..e82f7633be 100644 --- a/src/Windows/Avalonia.Win32/DirectX/DxgiConnection.cs +++ b/src/Windows/Avalonia.Win32/DirectX/DxgiConnection.cs @@ -10,11 +10,12 @@ using Avalonia.Logging; using Avalonia.OpenGL.Angle; using Avalonia.OpenGL.Egl; using Avalonia.Rendering; +using Avalonia.Win32.OpenGl.Angle; using static Avalonia.Win32.Interop.UnmanagedMethods; -using static Avalonia.Win32.DxgiSwapchain.DirectXUnmanagedMethods; +using static Avalonia.Win32.DirectX.DirectXUnmanagedMethods; using MicroCom.Runtime; -namespace Avalonia.Win32.DxgiSwapchain +namespace Avalonia.Win32.DirectX { #pragma warning disable CA1416 // This should only be reachable on Windows #nullable enable @@ -25,34 +26,27 @@ namespace Avalonia.Win32.DxgiSwapchain public bool RunsInBackground => true; public event Action? Tick; - - private AngleWin32EglDisplay _angle; - private EglPlatformOpenGlInterface _gl; private object _syncLock; private IDXGIOutput? _output = null; private Stopwatch? _stopwatch = null; + private const string LogArea = "DXGI"; - public DxgiConnection(EglPlatformOpenGlInterface gl, object syncLock) + public DxgiConnection(object syncLock) { - _syncLock = syncLock; - _angle = (AngleWin32EglDisplay)gl.Display; - _gl = gl; } - - public EglPlatformOpenGlInterface Egl => _gl; - - public static void TryCreateAndRegister(EglPlatformOpenGlInterface angle) + + public static void TryCreateAndRegister() { try { - TryCreateAndRegisterCore(angle); + TryCreateAndRegisterCore(); } catch (Exception ex) { - Logger.TryGet(LogEventLevel.Error, nameof(DxgiSwapchain)) + Logger.TryGet(LogEventLevel.Error, LogArea) ?.Log(null, "Unable to establish Dxgi: {0}", ex); } } @@ -66,7 +60,7 @@ namespace Avalonia.Win32.DxgiSwapchain } catch (Exception ex) { - Logger.TryGet(LogEventLevel.Error, nameof(DxgiSwapchain)) + Logger.TryGet(LogEventLevel.Error, LogArea) ?.Log(this, $"Failed to wait for vblank, Exception: {ex.Message}, HRESULT = {ex.HResult}"); } @@ -84,7 +78,7 @@ namespace Avalonia.Win32.DxgiSwapchain } catch (Exception ex) { - Logger.TryGet(LogEventLevel.Error, nameof(DxgiSwapchain)) + Logger.TryGet(LogEventLevel.Error, LogArea) ?.Log(this, $"Failed to wait for vblank, Exception: {ex.Message}, HRESULT = {ex.HResult}"); _output.Dispose(); _output = null; @@ -103,7 +97,7 @@ namespace Avalonia.Win32.DxgiSwapchain } catch (Exception ex) { - Logger.TryGet(LogEventLevel.Error, nameof(DxgiSwapchain)) + Logger.TryGet(LogEventLevel.Error, LogArea) ?.Log(this, $"Failed to wait for vblank, Exception: {ex.Message}, HRESULT = {ex.HResult}"); } } @@ -168,7 +162,7 @@ namespace Avalonia.Win32.DxgiSwapchain } // Used the windows composition as a blueprint for this startup/creation - static private bool TryCreateAndRegisterCore(EglPlatformOpenGlInterface gl) + static private bool TryCreateAndRegisterCore() { var tcs = new TaskCompletionSource(); var pumpLock = new object(); @@ -178,7 +172,7 @@ namespace Avalonia.Win32.DxgiSwapchain { DxgiConnection connection; - connection = new DxgiConnection(gl, pumpLock); + connection = new DxgiConnection(pumpLock); AvaloniaLocator.CurrentMutable.BindToSelf(connection); AvaloniaLocator.CurrentMutable.Bind().ToConstant(connection); diff --git a/src/Windows/Avalonia.Win32/DirectX/DxgiRenderTarget.cs b/src/Windows/Avalonia.Win32/DirectX/DxgiRenderTarget.cs index 2ab9b30d43..cb7826e185 100644 --- a/src/Windows/Avalonia.Win32/DirectX/DxgiRenderTarget.cs +++ b/src/Windows/Avalonia.Win32/DirectX/DxgiRenderTarget.cs @@ -8,11 +8,12 @@ using System.Threading.Tasks; using Avalonia.OpenGL.Angle; using Avalonia.OpenGL.Egl; using Avalonia.OpenGL.Surfaces; +using Avalonia.Win32.OpenGl.Angle; using MicroCom.Runtime; using static Avalonia.OpenGL.Egl.EglGlPlatformSurfaceBase; using static Avalonia.Win32.Interop.UnmanagedMethods; -namespace Avalonia.Win32.DxgiSwapchain +namespace Avalonia.Win32.DirectX { #pragma warning disable CA1416 // Validate platform compatibility, if you enter this not on windows you have messed up badly #nullable enable @@ -22,8 +23,7 @@ namespace Avalonia.Win32.DxgiSwapchain public const uint DXGI_USAGE_RENDER_TARGET_OUTPUT = 0x00000020U; - private IEglWindowGlPlatformSurfaceInfo _window; - private EglPlatformOpenGlInterface _egl; + private EglGlPlatformSurface.IEglWindowGlPlatformSurfaceInfo _window; private DxgiConnection _connection; private IDXGIDevice? _dxgiDevice = null; private IDXGIFactory2? _dxgiFactory = null; @@ -36,15 +36,14 @@ namespace Avalonia.Win32.DxgiSwapchain private Guid ID3D11Texture2DGuid = Guid.Parse("6F15AAF2-D208-4E89-9AB4-489535D34F9C"); - public DxgiRenderTarget(IEglWindowGlPlatformSurfaceInfo window, EglPlatformOpenGlInterface egl, DxgiConnection connection) : base(egl) + public DxgiRenderTarget(EglGlPlatformSurface.IEglWindowGlPlatformSurfaceInfo window, EglContext context, DxgiConnection connection) : base(context) { _window = window; - _egl = egl; _connection = connection; // the D3D device is expected to at least be an ID3D11Device // but how do I wrap an IntPtr as a managed IUnknown now? Like this. - IUnknown pdevice = MicroComRuntime.CreateProxyFor(((AngleWin32EglDisplay)_egl.Display).GetDirect3DDevice(), false); + IUnknown pdevice = MicroComRuntime.CreateProxyFor(((AngleWin32EglDisplay)context.Display).GetDirect3DDevice(), false); _dxgiDevice = pdevice.QueryInterface(); @@ -86,14 +85,14 @@ namespace Avalonia.Win32.DxgiSwapchain _clientRect = pClientRect; } - public override IGlPlatformSurfaceRenderingSession BeginDraw() + public override IGlPlatformSurfaceRenderingSession BeginDrawCore() { if (_swapChain is null) { throw new InvalidOperationException("No chain to draw on"); } - var contextLock = _egl.PrimaryContext.EnsureCurrent(); + var contextLock = Context.EnsureCurrent(); EglSurface? surface = null; IDisposable? transaction = null; var success = false; @@ -132,10 +131,10 @@ namespace Avalonia.Win32.DxgiSwapchain _renderTexture = texture; // I also have to get the pointer to this texture directly - surface = ((AngleWin32EglDisplay)_egl.Display).WrapDirect3D11Texture(_egl, MicroComRuntime.GetNativeIntPtr(_renderTexture), + surface = ((AngleWin32EglDisplay)Context.Display).WrapDirect3D11Texture(MicroComRuntime.GetNativeIntPtr(_renderTexture), 0, 0, size.Width, size.Height); - var res = base.BeginDraw(surface, _window, () => + var res = base.BeginDraw(surface, _window.Size, _window.Scaling, () => { _swapChain.Present((ushort)0U, (ushort)0U); surface?.Dispose(); diff --git a/src/Windows/Avalonia.Win32/DirectX/DxgiSwapchainWindow.cs b/src/Windows/Avalonia.Win32/DirectX/DxgiSwapchainWindow.cs index a40d8abf80..88226c5c89 100644 --- a/src/Windows/Avalonia.Win32/DirectX/DxgiSwapchainWindow.cs +++ b/src/Windows/Avalonia.Win32/DirectX/DxgiSwapchainWindow.cs @@ -3,29 +3,29 @@ using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; +using Avalonia.OpenGL; using Avalonia.OpenGL.Egl; using Avalonia.OpenGL.Surfaces; -namespace Avalonia.Win32.DxgiSwapchain +namespace Avalonia.Win32.DirectX { public class DxgiSwapchainWindow : EglGlPlatformSurfaceBase { private DxgiConnection _connection; - private EglPlatformOpenGlInterface _egl; - private IEglWindowGlPlatformSurfaceInfo _window; + private EglGlPlatformSurface.IEglWindowGlPlatformSurfaceInfo _window; - public DxgiSwapchainWindow(DxgiConnection connection, IEglWindowGlPlatformSurfaceInfo window) + public DxgiSwapchainWindow(DxgiConnection connection, EglGlPlatformSurface.IEglWindowGlPlatformSurfaceInfo window) { _connection = connection; _window = window; - _egl = connection.Egl; } - public override IGlPlatformSurfaceRenderTarget CreateGlRenderTarget() + public override IGlPlatformSurfaceRenderTarget CreateGlRenderTarget(IGlContext context) { - using (_egl.PrimaryContext.EnsureCurrent()) + var eglContext = (EglContext)context; + using (eglContext.EnsureCurrent()) { - return new DxgiRenderTarget(_window, _egl, _connection); + return new DxgiRenderTarget(_window, eglContext, _connection); } } } diff --git a/src/Windows/Avalonia.Win32/DirectX/IDirect3D11TexturePlatformSurface.cs b/src/Windows/Avalonia.Win32/DirectX/IDirect3D11TexturePlatformSurface.cs new file mode 100644 index 0000000000..d2a2b2f513 --- /dev/null +++ b/src/Windows/Avalonia.Win32/DirectX/IDirect3D11TexturePlatformSurface.cs @@ -0,0 +1,26 @@ +using System; +using Avalonia.OpenGL; +using Avalonia.Platform; + +namespace Avalonia.Win32.DirectX; + +public interface IDirect3D11TexturePlatformSurface +{ + public IDirect3D11TextureRenderTarget CreateRenderTarget(IPlatformGraphicsContext graphicsContext, IntPtr d3dDevice); +} + + + +public interface IDirect3D11TextureRenderTarget : IDisposable +{ + bool IsCorrupted { get; } + IDirect3D11TextureRenderTargetRenderSession BeginDraw(); +} + +public interface IDirect3D11TextureRenderTargetRenderSession : IDisposable +{ + public IntPtr D3D11Texture2D { get; } + public PixelSize Size { get; } + public PixelPoint Offset { get; } + public double Scaling { get; } +} diff --git a/src/Windows/Avalonia.Win32/DirectX/directx.idl b/src/Windows/Avalonia.Win32/DirectX/directx.idl index 09d1adf574..a4552eedea 100644 --- a/src/Windows/Avalonia.Win32/DirectX/directx.idl +++ b/src/Windows/Avalonia.Win32/DirectX/directx.idl @@ -1,4 +1,4 @@ -@clr-namespace Avalonia.Win32.DxgiSwapchain +@clr-namespace Avalonia.Win32.DirectX @clr-access internal @clr-map FLOAT float @clr-map HSTRING IntPtr @@ -12,6 +12,7 @@ @clr-map HWND IntPtr @clr-map BOOL int @clr-map DWORD int +@clr-map SIZE_T IntPtr @clr-map boolean int @clr-map BYTE byte @clr-map INT16 short @@ -242,9 +243,9 @@ interface IDXGIOutput : IDXGIObject HRESULT WaitForVBlank(); HRESULT TakeOwnership([in, annotation("_In_")] IUnknown* pDevice, BOOL Exclusive); void ReleaseOwnership(); - HRESULT GetGammaControlCapabilities([out, annotation("_Out_")] DXGI_GAMMA_CONTROL_CAPABILITIES* pGammaCaps); - HRESULT SetGammaControl([in, annotation("_In_")] DXGI_GAMMA_CONTROL* pArray); - HRESULT GetGammaControl([out, annotation("_Out_")] DXGI_GAMMA_CONTROL* pArray); + HRESULT GetGammaControlCapabilities(IntPtr pGammaCaps); + HRESULT SetGammaControl([in, annotation("_In_")] void* pArray); + HRESULT GetGammaControl(IntPtr pArray); HRESULT SetDisplaySurface([in, annotation("_In_")] IDXGISurface* pScanoutSurface); HRESULT GetDisplaySurfaceData([in, annotation("_In_")] IDXGISurface* pDestination); HRESULT GetFrameStatistics([out, annotation("_Out_")] DXGI_FRAME_STATISTICS* pStats); @@ -303,3 +304,180 @@ interface IDXGISwapChain1 : IDXGISwapChain HRESULT GetRotation([out, annotation("_Out_")] DXGI_MODE_ROTATION* pRotation); } +enum D3D11_FEATURE +{ + D3D11_FEATURE_THREADING, + D3D11_FEATURE_DOUBLES, + D3D11_FEATURE_FORMAT_SUPPORT, + D3D11_FEATURE_FORMAT_SUPPORT2, + D3D11_FEATURE_D3D10_X_HARDWARE_OPTIONS, + D3D11_FEATURE_D3D11_OPTIONS, + D3D11_FEATURE_ARCHITECTURE_INFO, + D3D11_FEATURE_D3D9_OPTIONS, + D3D11_FEATURE_SHADER_MIN_PRECISION_SUPPORT, + D3D11_FEATURE_D3D9_SHADOW_SUPPORT, + D3D11_FEATURE_D3D11_OPTIONS1, + D3D11_FEATURE_D3D9_SIMPLE_INSTANCING_SUPPORT, + D3D11_FEATURE_MARKER_SUPPORT, + D3D11_FEATURE_D3D9_OPTIONS1, + D3D11_FEATURE_D3D11_OPTIONS2, + D3D11_FEATURE_D3D11_OPTIONS3, + D3D11_FEATURE_GPU_VIRTUAL_ADDRESS_SUPPORT, + D3D11_FEATURE_D3D11_OPTIONSS, + D3D11_FEATURE_SHADER_CACHE +} + +[uuid(db6f6ddb-ac77-4e88-8253-819df9bbf140)] +interface ID3D11Device : IUnknown +{ + HRESULT CreateBuffer( + IntPtr pDesc, + IntPtr pInitialData, + [out, retval] IUnknown** ppBuffer ); + HRESULT CreateTexture1D( + IntPtr pDesc, + IntPtr pInitialData, + [out, retval] IUnknown** ppTexture1D ); + HRESULT CreateTexture2D( + IntPtr pDesc, + IntPtr pInitialData, + [out, retval] IUnknown** ppTexture2D ); + HRESULT CreateTexture3D( + IntPtr pDesc, + IntPtr pInitialData, + [out, retval] IUnknown** ppTexture3D ); + HRESULT CreateShaderResourceView( + IntPtr pResource, + IntPtr pDesc, + [out, retval] IUnknown** ppSRView ); + HRESULT CreateUnorderedAccessView( + IntPtr pResource, + IntPtr pDesc, + [out, retval] IUnknown** ppUAView ); + HRESULT CreateRenderTargetView( + IntPtr pResource, + IntPtr pDesc, + [out, retval] IUnknown** ppRTView); + HRESULT CreateDepthStencilView( + IntPtr pResource, + IntPtr pDesc, + [out, retval] IUnknown** ppDepthStencilView ); + HRESULT CreateInputLayout( + IntPtr pInputElementDescs, + UINT NumElements, + void* pShaderBytecodeWithInputSignature, + IntPtr BytecodeLength, + [out, retval] IUnknown** ppInputLayout ); + HRESULT CreateVertexShader( + IntPtr pShaderBytecode, + [annotation("_In_")] SIZE_T BytecodeLength, + IntPtr pClassLinkage, + [out, retval] IUnknown** ppVertexShader ); + HRESULT CreateGeometryShader( + IntPtr pShaderBytecode, + [annotation("_In_")] SIZE_T BytecodeLength, + IntPtr pClassLinkage, + [out, retval] IUnknown** ppGeometryShader ); + HRESULT CreateGeometryShaderWithStreamOutput( + IntPtr pShaderBytecode, + [annotation("_In_")] SIZE_T BytecodeLength, + IntPtr pSODeclaration, + UINT NumEntries, + UINT* pBufferStrides, + UINT NumStrides, + UINT RasterizedStream, + IntPtr pClassLinkage, + [out, retval] IUnknown** ppGeometryShader ); + HRESULT CreatePixelShader( + IntPtr pShaderBytecode, + [annotation("_In_")] SIZE_T BytecodeLength, + IntPtr pClassLinkage, + [out, retval] IUnknown** ppPixelShader ); + HRESULT CreateHullShader( + IntPtr pShaderBytecode, + [annotation("_In_")] SIZE_T BytecodeLength, + IntPtr pClassLinkage, + [out, retval] IUnknown** ppHullShader ); + HRESULT CreateDomainShader( + IntPtr pShaderBytecode, + [annotation("_In_")] SIZE_T BytecodeLength, + IntPtr pClassLinkage, + [out, retval] IUnknown** ppDomainShader ); + HRESULT CreateComputeShader( + IntPtr pShaderBytecode, + [annotation("_In_")] SIZE_T BytecodeLength, + IntPtr pClassLinkage, + [out, retval] IUnknown** ppComputeShader ); + HRESULT CreateClassLinkage([out, retval]IUnknown** ppLinkage); + HRESULT CreateBlendState( + IntPtr pBlendStateDesc, + [out, retval] IUnknown** ppBlendState ); + HRESULT CreateDepthStencilState( + IntPtr pDepthStencilDesc, + [out, retval] IUnknown** ppDepthStencilState ); + HRESULT CreateRasterizerState( + IntPtr pRasterizerDesc, + [out, retval] IUnknown** ppRasterizerState ); + HRESULT CreateSamplerState( + IntPtr pSamplerDesc, + [out, retval] IUnknown** ppSamplerState ); + HRESULT CreateQuery( + IntPtr pQueryDesc, + [out, retval] IUnknown** ppQuery ); + HRESULT CreatePredicate( + IntPtr pPredicateDesc, + [out, retval] IUnknown** ppPredicate ); + HRESULT CreateCounter( + IntPtr pCounterDesc, + [out, retval] IUnknown** ppCounter ); + HRESULT CreateDeferredContext( + UINT ContextFlags, // Reserved parameter; must be 0 + [out, retval] IUnknown** ppDeferredContext ); + HRESULT OpenSharedResource( + IntPtr hResource, + [out] Guid* ReturnedInterface, + [out, retval] IUnknown** ppResource); + + // Check* + HRESULT CheckFormatSupport( + [annotation("_In_")] DXGI_FORMAT Format, + [annotation("_Out_")] UINT* pFormatSupport ); + HRESULT CheckMultisampleQualityLevels( + [annotation("_In_")] DXGI_FORMAT Format, + [annotation("_In_")] UINT SampleCount, + [annotation("_Out_")] UINT* pNumQualityLevels ); + void CheckCounterInfo(IntPtr pCounterInfo ); + HRESULT CheckCounter( + IntPtr pDesc, + IntPtr pType, + IntPtr pActiveCounters, + IntPtr szName, + UINT* pNameLength, + IntPtr szUnits, + UINT* pUnitsLength, + IntPtr szDescription, + UINT* pDescriptionLength ); + HRESULT CheckFeatureSupport( + D3D11_FEATURE Feature, + void* pFeatureSupportData, + UINT FeatureSupportDataSize ); + + HRESULT GetPrivateData( + Guid* guid, + UINT* pDataSize, + void* pData ); + HRESULT SetPrivateData( + Guid* guid, + UINT DataSize, + IntPtr* pData ); + HRESULT SetPrivateDataInterface( + Guid* guid, + IUnknown* pData ); + + D3D_FEATURE_LEVEL GetFeatureLevel(); + UINT GetCreationFlags(); + int GetDeviceRemovedReason(); + void GetImmediateContext(IntPtr* ppImmediateContext ); + HRESULT SetExceptionMode( UINT RaiseFlags ); + UINT GetExceptionMode(); +} diff --git a/src/Windows/Avalonia.Win32/Interop/Automation/UiaCoreTypesApi.cs b/src/Windows/Avalonia.Win32/Interop/Automation/UiaCoreTypesApi.cs index 966f996a91..08b3ee32fa 100644 --- a/src/Windows/Avalonia.Win32/Interop/Automation/UiaCoreTypesApi.cs +++ b/src/Windows/Avalonia.Win32/Interop/Automation/UiaCoreTypesApi.cs @@ -6,8 +6,6 @@ namespace Avalonia.Win32.Interop.Automation { internal static class UiaCoreTypesApi { - private const string StartListeningExportName = "SynchronizedInputPattern_StartListening"; - internal enum AutomationIdType { Property, diff --git a/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs b/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs index 9b86154043..a7cca5b0f3 100644 --- a/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs +++ b/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs @@ -1627,7 +1627,7 @@ namespace Avalonia.Win32.Interop public static extern bool wglDeleteContext(IntPtr context); - [DllImport("opengl32.dll")] + [DllImport("opengl32.dll", SetLastError = true)] public static extern bool wglMakeCurrent(IntPtr hdc, IntPtr context); [DllImport("opengl32.dll")] diff --git a/src/Windows/Avalonia.Win32/OpenGl/Angle/AngleD3DTextureFeature.cs b/src/Windows/Avalonia.Win32/OpenGl/Angle/AngleD3DTextureFeature.cs new file mode 100644 index 0000000000..6c951010c8 --- /dev/null +++ b/src/Windows/Avalonia.Win32/OpenGl/Angle/AngleD3DTextureFeature.cs @@ -0,0 +1,103 @@ +using System.Runtime.InteropServices; +using Avalonia.OpenGL; +using Avalonia.OpenGL.Egl; +using Avalonia.OpenGL.Surfaces; +using Avalonia.Win32.DirectX; + +namespace Avalonia.Win32.OpenGl.Angle; + +internal class AngleD3DTextureFeature : IGlPlatformSurfaceRenderTargetFactory +{ + public bool CanRenderToSurface(IGlContext context, object surface) => + context is EglContext + { + Display: AngleWin32EglDisplay { PlatformApi: AngleOptions.PlatformApi.DirectX11 } + } && surface is IDirect3D11TexturePlatformSurface; + + class RenderTargetWrapper : EglPlatformSurfaceRenderTargetBase + { + private readonly AngleWin32EglDisplay _angle; + private readonly IDirect3D11TextureRenderTarget _target; + + public RenderTargetWrapper(EglContext context, + AngleWin32EglDisplay angle, + IDirect3D11TextureRenderTarget target) : base(context) + { + _angle = angle; + _target = target; + } + + public override IGlPlatformSurfaceRenderingSession BeginDrawCore() + { + var success = false; + var contextLock = Context.EnsureCurrent(); + IDirect3D11TextureRenderTargetRenderSession session = null; + EglSurface surface = null; + try + { + try + { + session = _target.BeginDraw(); + } + catch (RenderTargetCorruptedException e) + { + if (e.InnerException is COMException com + && ((DXGI_ERROR)com.HResult).IsDeviceLostError()) + Context.NotifyContextLost(); + + throw; + } + + surface = _angle.WrapDirect3D11Texture(session.D3D11Texture2D, session.Offset.X, session.Offset.Y, + session.Size.Width, session.Size.Height); + var rv = BeginDraw(surface, session.Size, session.Scaling, () => + { + using(contextLock) + using (session) + using (surface) + { + } + }, true); + success = true; + return rv; + } + finally + { + if (!success) + { + using(contextLock) + using (session) + using (surface) + { + } + } + } + } + + public override void Dispose() + { + _target.Dispose(); + base.Dispose(); + } + + public override bool IsCorrupted => _target.IsCorrupted || base.IsCorrupted; + } + + public IGlPlatformSurfaceRenderTarget CreateRenderTarget(IGlContext context, object surface) + { + var ctx = (EglContext)context; + var angle = (AngleWin32EglDisplay)ctx.Display; + var textureSurface = (IDirect3D11TexturePlatformSurface)surface; + try + { + var target = textureSurface.CreateRenderTarget(context, angle.GetDirect3DDevice()); + return new RenderTargetWrapper(ctx, angle, target); + } + catch (COMException com) + { + if (((DXGI_ERROR)com.HResult).IsDeviceLostError()) + ctx.NotifyContextLost(); + throw; + } + } +} diff --git a/src/Avalonia.OpenGL/Angle/AngleEglInterface.cs b/src/Windows/Avalonia.Win32/OpenGl/Angle/AngleEglInterface.cs similarity index 56% rename from src/Avalonia.OpenGL/Angle/AngleEglInterface.cs rename to src/Windows/Avalonia.Win32/OpenGl/Angle/AngleEglInterface.cs index 3a332f37ad..4e00bc2c72 100644 --- a/src/Avalonia.OpenGL/Angle/AngleEglInterface.cs +++ b/src/Windows/Avalonia.Win32/OpenGl/Angle/AngleEglInterface.cs @@ -1,18 +1,31 @@ using System; using System.Runtime.InteropServices; using Avalonia.OpenGL.Egl; +using Avalonia.SourceGenerator; namespace Avalonia.OpenGL.Angle { - public class AngleEglInterface : EglInterface + internal partial class Win32AngleEglInterface : EglInterface { [DllImport("av_libGLESv2.dll", CharSet = CharSet.Ansi)] static extern IntPtr EGL_GetProcAddress(string proc); - public AngleEglInterface() : base(LoadAngle()) - { + public Win32AngleEglInterface() : this(LoadAngle()) + { + } + + private Win32AngleEglInterface(Func getProcAddress) : base(getProcAddress) + { + Initialize(getProcAddress); + } + + [GetProcAddress("eglCreateDeviceANGLE", true)] + public partial IntPtr CreateDeviceANGLE(int deviceType, IntPtr nativeDevice, int[] attribs); + + [GetProcAddress("eglReleaseDeviceANGLE", true)] + public partial void ReleaseDeviceANGLE(IntPtr device); static Func LoadAngle() { diff --git a/src/Windows/Avalonia.Win32/OpenGl/Angle/AngleWin32EglDisplay.cs b/src/Windows/Avalonia.Win32/OpenGl/Angle/AngleWin32EglDisplay.cs new file mode 100644 index 0000000000..b2d7b2014b --- /dev/null +++ b/src/Windows/Avalonia.Win32/OpenGl/Angle/AngleWin32EglDisplay.cs @@ -0,0 +1,140 @@ +using System; +using System.ComponentModel; +using System.Runtime.InteropServices; +using Avalonia.OpenGL; +using Avalonia.OpenGL.Angle; +using Avalonia.OpenGL.Egl; +using Avalonia.Win32.DirectX; +using MicroCom.Runtime; +using static Avalonia.OpenGL.Egl.EglConsts; + +namespace Avalonia.Win32.OpenGl.Angle +{ + internal class AngleWin32EglDisplay : EglDisplay + { + protected override bool DisplayLockIsSharedWithContexts => true; + + public static AngleWin32EglDisplay CreateD3D9Display(EglInterface egl) + { + var display = egl.GetPlatformDisplayExt(EGL_PLATFORM_ANGLE_ANGLE, IntPtr.Zero, + new[] { EGL_PLATFORM_ANGLE_TYPE_ANGLE, EGL_PLATFORM_ANGLE_TYPE_D3D9_ANGLE, EGL_NONE }); + + return new AngleWin32EglDisplay(display, new EglDisplayOptions() + { + Egl = egl, + ContextLossIsDisplayLoss = true, + GlVersions = AvaloniaLocator.Current.GetService()?.GlProfiles + }, AngleOptions.PlatformApi.DirectX9); + } + + public static AngleWin32EglDisplay CreateSharedD3D11Display(EglInterface egl) + { + var display = egl.GetPlatformDisplayExt(EGL_PLATFORM_ANGLE_ANGLE, IntPtr.Zero, + new[] { EGL_PLATFORM_ANGLE_TYPE_ANGLE, EGL_PLATFORM_ANGLE_TYPE_D3D11_ANGLE, EGL_NONE }); + + return new AngleWin32EglDisplay(display, new EglDisplayOptions() + { + Egl = egl, + ContextLossIsDisplayLoss = true, + GlVersions = AvaloniaLocator.Current.GetService()?.GlProfiles + }, AngleOptions.PlatformApi.DirectX11); + } + + public static AngleWin32EglDisplay CreateD3D11Display(Win32AngleEglInterface egl) + { + unsafe + { + var featureLevels = new[] + { + D3D_FEATURE_LEVEL.D3D_FEATURE_LEVEL_11_1, + D3D_FEATURE_LEVEL.D3D_FEATURE_LEVEL_11_0, + D3D_FEATURE_LEVEL.D3D_FEATURE_LEVEL_10_1, + D3D_FEATURE_LEVEL.D3D_FEATURE_LEVEL_10_0, + D3D_FEATURE_LEVEL.D3D_FEATURE_LEVEL_9_3, + D3D_FEATURE_LEVEL.D3D_FEATURE_LEVEL_9_2, + D3D_FEATURE_LEVEL.D3D_FEATURE_LEVEL_9_1 + }; + + DirectXUnmanagedMethods.D3D11CreateDevice(IntPtr.Zero, D3D_DRIVER_TYPE.D3D_DRIVER_TYPE_HARDWARE, + IntPtr.Zero, 0, featureLevels, (uint)featureLevels.Length, + 7, out var pD3dDevice, out var featureLevel, null); + if (pD3dDevice == IntPtr.Zero) + throw new Win32Exception("Unable to create D3D11 Device"); + + var d3dDevice = MicroComRuntime.CreateProxyFor(pD3dDevice, true); + var angleDevice = IntPtr.Zero; + var display = IntPtr.Zero; + + void Cleanup() + { + if (angleDevice != IntPtr.Zero) + egl.ReleaseDeviceANGLE(angleDevice); + d3dDevice.Dispose(); + } + + bool success = false; + try + { + angleDevice = egl.CreateDeviceANGLE(EGL_D3D11_DEVICE_ANGLE, pD3dDevice, null); + if (angleDevice == IntPtr.Zero) + throw OpenGlException.GetFormattedException("eglCreateDeviceANGLE", egl); + + display = egl.GetPlatformDisplayExt(EGL_PLATFORM_DEVICE_EXT, angleDevice, null); + if (display == IntPtr.Zero) + throw OpenGlException.GetFormattedException("eglGetPlatformDisplayEXT", egl); + + + var rv = new AngleWin32EglDisplay(display, new EglDisplayOptions + { + DisposeCallback = Cleanup, + Egl = egl, + ContextLossIsDisplayLoss = true, + DeviceLostCheckCallback = () => d3dDevice.DeviceRemovedReason != 0, + GlVersions = AvaloniaLocator.Current.GetService()?.GlProfiles + }, AngleOptions.PlatformApi.DirectX11); + success = true; + return rv; + } + finally + { + if (!success) + { + if (display != IntPtr.Zero) + egl.Terminate(display); + Cleanup(); + } + } + } + } + + private AngleWin32EglDisplay(IntPtr display, EglDisplayOptions options, AngleOptions.PlatformApi platformApi) : base(display, options) + { + PlatformApi = platformApi; + } + + public AngleOptions.PlatformApi PlatformApi { get; } + + public IntPtr GetDirect3DDevice() + { + if (!EglInterface.QueryDisplayAttribExt(Handle, EglConsts.EGL_DEVICE_EXT, out var eglDevice)) + throw new OpenGlException("Unable to get EGL_DEVICE_EXT"); + if (!EglInterface.QueryDeviceAttribExt(eglDevice, PlatformApi == AngleOptions.PlatformApi.DirectX9 ? EGL_D3D9_DEVICE_ANGLE : EGL_D3D11_DEVICE_ANGLE, out var d3dDeviceHandle)) + throw new OpenGlException("Unable to get EGL_D3D9_DEVICE_ANGLE"); + return d3dDeviceHandle; + } + + public EglSurface WrapDirect3D11Texture( IntPtr handle) + { + if (PlatformApi != AngleOptions.PlatformApi.DirectX11) + throw new InvalidOperationException("Current platform API is " + PlatformApi); + return CreatePBufferFromClientBuffer(EGL_D3D_TEXTURE_ANGLE, handle, new[] { EGL_NONE, EGL_NONE }); + } + + public EglSurface WrapDirect3D11Texture(IntPtr handle, int offsetX, int offsetY, int width, int height) + { + if (PlatformApi != AngleOptions.PlatformApi.DirectX11) + throw new InvalidOperationException("Current platform API is " + PlatformApi); + return CreatePBufferFromClientBuffer(EGL_D3D_TEXTURE_ANGLE, handle, new[] { EGL_WIDTH, width, EGL_HEIGHT, height, EGL_FLEXIBLE_SURFACE_COMPATIBILITY_SUPPORTED_ANGLE, EGL_TRUE, EGL_TEXTURE_OFFSET_X_ANGLE, offsetX, EGL_TEXTURE_OFFSET_Y_ANGLE, offsetY, EGL_NONE }); + } + } +} diff --git a/src/Windows/Avalonia.Win32/OpenGl/Angle/AngleWin32PlatformGraphics.cs b/src/Windows/Avalonia.Win32/OpenGl/Angle/AngleWin32PlatformGraphics.cs new file mode 100644 index 0000000000..9a829aff92 --- /dev/null +++ b/src/Windows/Avalonia.Win32/OpenGl/Angle/AngleWin32PlatformGraphics.cs @@ -0,0 +1,131 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Avalonia.Logging; +using Avalonia.OpenGL; +using Avalonia.OpenGL.Angle; +using Avalonia.OpenGL.Egl; +using Avalonia.Platform; + +namespace Avalonia.Win32.OpenGl.Angle; + +internal class AngleWin32PlatformGraphics : IPlatformGraphics +{ + private readonly Win32AngleEglInterface _egl; + private AngleWin32EglDisplay _sharedDisplay; + private EglContext _sharedContext; + public bool UsesSharedContext => PlatformApi == AngleOptions.PlatformApi.DirectX9; + + public AngleOptions.PlatformApi PlatformApi { get; } = AngleOptions.PlatformApi.DirectX11; + public IPlatformGraphicsContext CreateContext() + { + if (UsesSharedContext) + throw new InvalidOperationException(); + + var display = AngleWin32EglDisplay.CreateD3D11Display(_egl); + var success = false; + try + { + var rv = display.CreateContext(new EglContextOptions + { + DisposeCallback = display.Dispose, + ExtraFeatures = new Dictionary + { + [typeof(IGlPlatformSurfaceRenderTargetFactory)] = new AngleD3DTextureFeature() + } + }); + success = true; + return rv; + } + finally + { + if (!success) + display.Dispose(); + } + } + + public IPlatformGraphicsContext GetSharedContext() + { + if (!UsesSharedContext) + throw new InvalidOperationException(); + if (_sharedContext == null || _sharedContext.IsLost) + { + _sharedContext?.Dispose(); + _sharedContext = null; + _sharedContext = _sharedDisplay.CreateContext(new EglContextOptions()); + } + + return _sharedContext; + } + + public AngleWin32PlatformGraphics(Win32AngleEglInterface egl, AngleWin32EglDisplay display) + : this(egl, display.PlatformApi) + { + _sharedDisplay = display; + } + + public AngleWin32PlatformGraphics(Win32AngleEglInterface egl, AngleOptions.PlatformApi api) + { + _egl = egl; + PlatformApi = api; + } + + + public static AngleWin32PlatformGraphics TryCreate(AngleOptions options) + { + + + Win32AngleEglInterface egl; + try + { + egl = new(); + } + catch (Exception e) + { + Logger.TryGet(LogEventLevel.Error, "OpenGL") + ?.Log(null, "Unable to load ANGLE: {0}", e); + return null; + } + + foreach (var api in (options?.AllowedPlatformApis ?? new [] + { + AngleOptions.PlatformApi.DirectX11 + }).Distinct()) + if (api == AngleOptions.PlatformApi.DirectX11) + { + try + { + using var display = AngleWin32EglDisplay.CreateD3D11Display(egl); + using var ctx = display.CreateContext(new EglContextOptions()); + ctx.MakeCurrent().Dispose(); + } + catch (Exception e) + { + Logger.TryGet(LogEventLevel.Error, "OpenGL") + ?.Log(null, "Unable to initialize ANGLE-based rendering with DirectX11 : {0}", e); + continue; + } + + return new AngleWin32PlatformGraphics(egl, AngleOptions.PlatformApi.DirectX11); + } + else + { + AngleWin32EglDisplay sharedDisplay = null; + try + { + sharedDisplay = AngleWin32EglDisplay.CreateD3D9Display(egl); + using (var ctx = sharedDisplay.CreateContext(new EglContextOptions())) + ctx.MakeCurrent().Dispose(); + + return new AngleWin32PlatformGraphics(egl, sharedDisplay); + } + catch (Exception e) + { + sharedDisplay?.Dispose(); + Logger.TryGet(LogEventLevel.Error, "OpenGL") + ?.Log(null, "Unable to initialize ANGLE-based rendering with DirectX9 : {0}", e); + } + } + return null; + } +} diff --git a/src/Windows/Avalonia.Win32/OpenGl/WglContext.cs b/src/Windows/Avalonia.Win32/OpenGl/WglContext.cs index d6633ddb61..d535efc5b7 100644 --- a/src/Windows/Avalonia.Win32/OpenGl/WglContext.cs +++ b/src/Windows/Avalonia.Win32/OpenGl/WglContext.cs @@ -1,6 +1,10 @@ using System; +using System.Collections.Generic; +using System.Linq; using System.Reactive.Disposables; +using System.Threading; using Avalonia.OpenGL; +using Avalonia.Platform; using Avalonia.Win32.Interop; using static Avalonia.Win32.Interop.UnmanagedMethods; using static Avalonia.Win32.OpenGl.WglConsts; @@ -43,8 +47,9 @@ namespace Avalonia.Win32.OpenGl public void Dispose() { wglDeleteContext(_context); - ReleaseDC(_hWnd, _dc); - DestroyWindow(_hWnd); + WglGdiResourceManager.ReleaseDC(_hWnd, _dc); + WglGdiResourceManager.DestroyWindow(_hWnd); + IsLost = true; } public GlVersion Version { get; } @@ -55,17 +60,26 @@ namespace Avalonia.Win32.OpenGl private bool IsCurrent => wglGetCurrentContext() == _context && wglGetCurrentDC() == _dc; public IDisposable MakeCurrent() { + if (IsLost) + throw new PlatformGraphicsContextLostException(); if(IsCurrent) return Disposable.Empty; return new WglRestoreContext(_dc, _context, _lock); } + public bool IsLost { get; private set; } public IDisposable EnsureCurrent() => MakeCurrent(); + internal IDisposable Lock() + { + Monitor.Enter(_lock); + return Disposable.Create(_lock, Monitor.Exit); + } + public IntPtr CreateConfiguredDeviceContext(IntPtr hWnd) { - var dc = GetDC(hWnd); + var dc = WglGdiResourceManager.GetDC(hWnd); var fmt = _formatDescriptor; SetPixelFormat(dc, _pixelFormat, ref fmt); return dc; @@ -81,5 +95,14 @@ namespace Avalonia.Win32.OpenGl || _sharedWith == context || _sharedWith != null && _sharedWith == c._sharedWith; } + + public bool CanCreateSharedContext => true; + public IGlContext CreateSharedContext(IEnumerable preferredVersions = null) + { + var versions = preferredVersions?.Append(Version).ToArray() ?? new[] { Version }; + return WglDisplay.CreateContext(versions, _sharedWith ?? this); + } + + public object TryGetFeature(Type featureType) => null; } } diff --git a/src/Windows/Avalonia.Win32/OpenGl/WglDisplay.cs b/src/Windows/Avalonia.Win32/OpenGl/WglDisplay.cs index bc27589689..9a1963a97a 100644 --- a/src/Windows/Avalonia.Win32/OpenGl/WglDisplay.cs +++ b/src/Windows/Avalonia.Win32/OpenGl/WglDisplay.cs @@ -1,6 +1,7 @@ using System; using System.Runtime.InteropServices; using Avalonia.OpenGL; +using Avalonia.Threading; using Avalonia.Win32.Interop; using static Avalonia.Win32.Interop.UnmanagedMethods; using static Avalonia.Win32.OpenGl.WglConsts; @@ -9,8 +10,6 @@ namespace Avalonia.Win32.OpenGl internal class WglDisplay { private static bool? _initialized; - private static ushort _windowClass; - private static readonly WndProc _wndProcDelegate = WndProc; private static readonly DebugCallbackDelegate _debugCallback = DebugCallback; private static IntPtr _bootstrapContext; @@ -44,18 +43,9 @@ namespace Avalonia.Win32.OpenGl } static bool InitializeCore() { - var wndClassEx = new WNDCLASSEX - { - cbSize = Marshal.SizeOf(), - hInstance = GetModuleHandle(null), - lpfnWndProc = _wndProcDelegate, - lpszClassName = "AvaloniaGlWindow-" + Guid.NewGuid(), - style = (int)ClassStyles.CS_OWNDC - }; - - _windowClass = RegisterClassEx(ref wndClassEx); - _bootstrapWindow = CreateOffscreenWindow(); - _bootstrapDc = GetDC(_bootstrapWindow); + Dispatcher.UIThread.VerifyAccess(); + _bootstrapWindow = WglGdiResourceManager.CreateOffscreenWindow(); + _bootstrapDc = WglGdiResourceManager.GetDC(_bootstrapWindow); _defaultPfd = new PixelFormatDescriptor { Size = (ushort)Marshal.SizeOf(), @@ -105,17 +95,11 @@ namespace Avalonia.Win32.OpenGl DescribePixelFormat(_bootstrapDc, formats[0], Marshal.SizeOf(), ref _defaultPfd); _defaultPixelFormat = formats[0]; } - - + wglMakeCurrent(IntPtr.Zero, IntPtr.Zero); return true; } - - static IntPtr WndProc(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam) - { - return DefWindowProc(hWnd, msg, wParam, lParam); - } - + private static void DebugCallback(int source, int type, int id, int severity, int len, IntPtr message, IntPtr userparam) { var err = Marshal.PtrToStringAnsi(message, len); @@ -131,27 +115,32 @@ namespace Avalonia.Win32.OpenGl using (new WglRestoreContext(_bootstrapDc, _bootstrapContext, null)) { - var window = CreateOffscreenWindow(); - var dc = GetDC(window); + var window = WglGdiResourceManager.CreateOffscreenWindow(); + var dc = WglGdiResourceManager.GetDC(window); SetPixelFormat(dc, _defaultPixelFormat, ref _defaultPfd); foreach (var version in versions) { if(version.Type != GlProfileType.OpenGL) continue; - var context = WglCreateContextAttribsArb(dc, shareContext?.Handle ?? IntPtr.Zero, - new[] - { - // major - WGL_CONTEXT_MAJOR_VERSION_ARB, version.Major, - // minor - WGL_CONTEXT_MINOR_VERSION_ARB, version.Minor, - // core profile - WGL_CONTEXT_PROFILE_MASK_ARB, 1, - // debug - // WGL_CONTEXT_FLAGS_ARB, 1, - // end - 0, 0 - }); + IntPtr context; + using (shareContext?.Lock()) + { + context = WglCreateContextAttribsArb(dc, shareContext?.Handle ?? IntPtr.Zero, + new[] + { + // major + WGL_CONTEXT_MAJOR_VERSION_ARB, version.Major, + // minor + WGL_CONTEXT_MINOR_VERSION_ARB, version.Minor, + // core profile + WGL_CONTEXT_PROFILE_MASK_ARB, 1, + // debug + // WGL_CONTEXT_FLAGS_ARB, 1, + // end + 0, 0 + }); + } + using(new WglRestoreContext(dc, context, null)) GlDebugMessageCallback(Marshal.GetFunctionPointerForDelegate(_debugCallback), IntPtr.Zero); if (context != IntPtr.Zero) @@ -159,26 +148,13 @@ namespace Avalonia.Win32.OpenGl _defaultPixelFormat, _defaultPfd); } - ReleaseDC(window, dc); - DestroyWindow(window); + WglGdiResourceManager.ReleaseDC(window, dc); + WglGdiResourceManager.DestroyWindow(window); return null; } } - static IntPtr CreateOffscreenWindow() => - CreateWindowEx( - 0, - _windowClass, - null, - (int)WindowStyles.WS_OVERLAPPEDWINDOW, - 0, - 0, - 640, - 480, - IntPtr.Zero, - IntPtr.Zero, - IntPtr.Zero, - IntPtr.Zero); + } } diff --git a/src/Windows/Avalonia.Win32/OpenGl/WglGdiResourceManager.cs b/src/Windows/Avalonia.Win32/OpenGl/WglGdiResourceManager.cs new file mode 100644 index 0000000000..ed40488c70 --- /dev/null +++ b/src/Windows/Avalonia.Win32/OpenGl/WglGdiResourceManager.cs @@ -0,0 +1,166 @@ +using System; +using System.Collections.Generic; +using System.Runtime.InteropServices; +using System.Threading; +using System.Threading.Tasks; +using Avalonia.Win32.Interop; + +namespace Avalonia.Win32.OpenGl; + +/// +/// - ReleaseDC can only happen from the same thread that has called GetDC +/// - When thread exits all of its windows and HDCs are getting destroyed +/// - We need to create OpenGL context (require a window and an HDC) and render targets (require an HDC) from thread pool threads +/// +/// So this class hosts a dedicated thread for managing offscreen windows and HDCs for OpenGL +/// + +internal class WglGdiResourceManager +{ + class GetDCOp + { + public IntPtr Window; + public TaskCompletionSource Result; + } + + class ReleaseDCOp + { + public IntPtr Window; + public IntPtr DC; + public TaskCompletionSource Result; + } + + class CreateWindowOp + { + public TaskCompletionSource Result; + } + + class DestroyWindowOp + { + public IntPtr Window; + public TaskCompletionSource Result; + } + + private static readonly Queue s_Queue = new(); + private static readonly AutoResetEvent s_Event = new(false); + private static readonly ushort s_WindowClass; + private static readonly UnmanagedMethods.WndProc s_wndProcDelegate = WndProc; + + static void Worker() + { + while (true) + { + s_Event.WaitOne(); + lock (s_Queue) + { + if(s_Queue.Count == 0) + continue; + var job = s_Queue.Dequeue(); + if (job is GetDCOp getDc) + getDc.Result.TrySetResult(UnmanagedMethods.GetDC(getDc.Window)); + else if (job is ReleaseDCOp releaseDc) + { + UnmanagedMethods.ReleaseDC(releaseDc.Window, releaseDc.DC); + releaseDc.Result.SetResult(null); + } + else if (job is CreateWindowOp createWindow) + createWindow.Result.TrySetResult(UnmanagedMethods.CreateWindowEx( + 0, + s_WindowClass, + null, + (int)UnmanagedMethods.WindowStyles.WS_OVERLAPPEDWINDOW, + 0, + 0, + 640, + 480, + IntPtr.Zero, + IntPtr.Zero, + IntPtr.Zero, + IntPtr.Zero)); + else if (job is DestroyWindowOp destroyWindow) + { + UnmanagedMethods.DestroyWindow(destroyWindow.Window); + destroyWindow.Result.TrySetResult(null); + } + } + } + } + + static WglGdiResourceManager() + { + var wndClassEx = new UnmanagedMethods.WNDCLASSEX + { + cbSize = Marshal.SizeOf(), + hInstance = UnmanagedMethods.GetModuleHandle(null), + lpfnWndProc = s_wndProcDelegate, + lpszClassName = "AvaloniaGlWindow-" + Guid.NewGuid(), + style = (int)UnmanagedMethods.ClassStyles.CS_OWNDC + }; + + s_WindowClass = UnmanagedMethods.RegisterClassEx(ref wndClassEx); + var th = new Thread(Worker) { IsBackground = true, Name = "Win32 OpenGL HDC manager" }; + // This makes CLR to automatically pump the event queue from WaitOne + th.SetApartmentState(ApartmentState.STA); + th.Start(); + } + + + + static IntPtr WndProc(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam) + { + return UnmanagedMethods.DefWindowProc(hWnd, msg, wParam, lParam); + } + + public static IntPtr CreateOffscreenWindow() + { + var tcs = new TaskCompletionSource(); + lock(s_Queue) + s_Queue.Enqueue(new CreateWindowOp() + { + Result = tcs + }); + s_Event.Set(); + return tcs.Task.Result; + } + + public static IntPtr GetDC(IntPtr hWnd) + { + var tcs = new TaskCompletionSource(); + lock(s_Queue) + s_Queue.Enqueue(new GetDCOp + { + Window = hWnd, + Result = tcs + }); + s_Event.Set(); + return tcs.Task.Result; + } + + public static void ReleaseDC(IntPtr hWnd, IntPtr hDC) + { + var tcs = new TaskCompletionSource(); + lock(s_Queue) + s_Queue.Enqueue(new ReleaseDCOp() + { + Window = hWnd, + DC = hDC, + Result = tcs + }); + s_Event.Set(); + tcs.Task.Wait(); + } + + public static void DestroyWindow(IntPtr hWnd) + { + var tcs = new TaskCompletionSource(); + lock(s_Queue) + s_Queue.Enqueue(new DestroyWindowOp() + { + Window = hWnd, + Result = tcs + }); + s_Event.Set(); + tcs.Task.Wait(); + + } +} diff --git a/src/Windows/Avalonia.Win32/OpenGl/WglGlPlatformSurface.cs b/src/Windows/Avalonia.Win32/OpenGl/WglGlPlatformSurface.cs index 72bcffd447..6d3d2d28c4 100644 --- a/src/Windows/Avalonia.Win32/OpenGl/WglGlPlatformSurface.cs +++ b/src/Windows/Avalonia.Win32/OpenGl/WglGlPlatformSurface.cs @@ -11,18 +11,16 @@ namespace Avalonia.Win32.OpenGl class WglGlPlatformSurface: IGlPlatformSurface { - private readonly WglContext _context; private readonly EglGlPlatformSurface.IEglWindowGlPlatformSurfaceInfo _info; - public WglGlPlatformSurface(WglContext context, EglGlPlatformSurface.IEglWindowGlPlatformSurfaceInfo info) + public WglGlPlatformSurface( EglGlPlatformSurface.IEglWindowGlPlatformSurfaceInfo info) { - _context = context; _info = info; } - public IGlPlatformSurfaceRenderTarget CreateGlRenderTarget() + public IGlPlatformSurfaceRenderTarget CreateGlRenderTarget(IGlContext context) { - return new RenderTarget(_context, _info); + return new RenderTarget((WglContext)context, _info); } class RenderTarget : IGlPlatformSurfaceRenderTarget @@ -39,7 +37,7 @@ namespace Avalonia.Win32.OpenGl public void Dispose() { - UnmanagedMethods.ReleaseDC(_hdc, _info.Handle); + WglGdiResourceManager.ReleaseDC(_info.Handle, _hdc); } public IGlPlatformSurfaceRenderingSession BeginDraw() diff --git a/src/Windows/Avalonia.Win32/OpenGl/WglPlatformOpenGlInterface.cs b/src/Windows/Avalonia.Win32/OpenGl/WglPlatformOpenGlInterface.cs index 1d0880a468..39dd330d52 100644 --- a/src/Windows/Avalonia.Win32/OpenGl/WglPlatformOpenGlInterface.cs +++ b/src/Windows/Avalonia.Win32/OpenGl/WglPlatformOpenGlInterface.cs @@ -6,15 +6,13 @@ using Avalonia.Platform; namespace Avalonia.Win32.OpenGl { - class WglPlatformOpenGlInterface : IPlatformOpenGlInterface + class WglPlatformOpenGlInterface : IPlatformGraphics { public WglContext PrimaryContext { get; } - IPlatformGpuContext IPlatformGpu.PrimaryContext => PrimaryContext; - IGlContext IPlatformOpenGlInterface.PrimaryContext => PrimaryContext; - public IGlContext CreateSharedContext() => WglDisplay.CreateContext(new[] { PrimaryContext.Version }, PrimaryContext); - - public bool CanShareContexts => true; - public bool CanCreateContexts => true; + public bool UsesSharedContext => false; + IPlatformGraphicsContext IPlatformGraphics.CreateContext() => CreateContext(); + public IPlatformGraphicsContext GetSharedContext() => throw new NotSupportedException(); + public IGlContext CreateContext() => WglDisplay.CreateContext(new[] { PrimaryContext.Version }, null); private WglPlatformOpenGlInterface(WglContext primary) diff --git a/src/Windows/Avalonia.Win32/OpenGl/WglRestoreContext.cs b/src/Windows/Avalonia.Win32/OpenGl/WglRestoreContext.cs index 265f078a5c..b145ffbb37 100644 --- a/src/Windows/Avalonia.Win32/OpenGl/WglRestoreContext.cs +++ b/src/Windows/Avalonia.Win32/OpenGl/WglRestoreContext.cs @@ -1,4 +1,5 @@ using System; +using System.Runtime.InteropServices; using System.Threading; using Avalonia.OpenGL; using static Avalonia.Win32.Interop.UnmanagedMethods; @@ -22,9 +23,11 @@ namespace Avalonia.Win32.OpenGl if (!wglMakeCurrent(gc, context)) { + var lastError = Marshal.GetLastWin32Error(); + var caps = GetDeviceCaps(gc, (DEVICECAP)12); if(monitor != null && takeMonitor) Monitor.Exit(monitor); - throw new OpenGlException("Unable to make the context current"); + throw new OpenGlException($"Unable to make the context current: {lastError}, DC valid: {caps != 0}"); } } diff --git a/src/Windows/Avalonia.Win32/Win32GlManager.cs b/src/Windows/Avalonia.Win32/Win32GlManager.cs index 204acc82c9..25ea060576 100644 --- a/src/Windows/Avalonia.Win32/Win32GlManager.cs +++ b/src/Windows/Avalonia.Win32/Win32GlManager.cs @@ -2,8 +2,9 @@ using Avalonia.OpenGL; using Avalonia.OpenGL.Angle; using Avalonia.OpenGL.Egl; using Avalonia.Platform; -using Avalonia.Win32.DxgiSwapchain; +using Avalonia.Win32.DirectX; using Avalonia.Win32.OpenGl; +using Avalonia.Win32.OpenGl.Angle; using Avalonia.Win32.WinRT.Composition; namespace Avalonia.Win32 @@ -11,15 +12,15 @@ namespace Avalonia.Win32 static class Win32GlManager { - public static IPlatformOpenGlInterface Initialize() + public static IPlatformGraphics Initialize() { var gl = InitializeCore(); - AvaloniaLocator.CurrentMutable.Bind().ToConstant(gl); - AvaloniaLocator.CurrentMutable.Bind().ToConstant(gl); + AvaloniaLocator.CurrentMutable.Bind().ToConstant(gl); + AvaloniaLocator.CurrentMutable.Bind().ToConstant(gl); return gl; } - static IPlatformOpenGlInterface InitializeCore() + static IPlatformGraphics InitializeCore() { var opts = AvaloniaLocator.Current.GetService() ?? new Win32PlatformOptions(); @@ -31,28 +32,18 @@ namespace Avalonia.Win32 if (opts.AllowEglInitialization ?? Win32Platform.WindowsVersion > PlatformConstants.Windows7) { - var egl = EglPlatformOpenGlInterface.TryCreate(() => new AngleWin32EglDisplay()); + var egl = AngleWin32PlatformGraphics.TryCreate(AvaloniaLocator.Current.GetService() ?? + new()); - if (egl != null) + if (egl != null && egl.PlatformApi == AngleOptions.PlatformApi.DirectX11) { - if (opts.EglRendererBlacklist != null) - { - foreach (var item in opts.EglRendererBlacklist) - { - if (egl.PrimaryEglContext.GlInterface.Renderer.Contains(item)) - { - return null; - } - } - } - if (opts.UseWindowsUIComposition) { - WinUICompositorConnection.TryCreateAndRegister(egl, opts.CompositionBackdropCornerRadius); + WinUiCompositorConnection.TryCreateAndRegister(); } else if (opts.UseLowLatencyDxgiSwapChain) { - DxgiConnection.TryCreateAndRegister(egl); + DxgiConnection.TryCreateAndRegister(); } } diff --git a/src/Windows/Avalonia.Win32/Win32Platform.cs b/src/Windows/Avalonia.Win32/Win32Platform.cs index 63cad9679a..3f220f0f09 100644 --- a/src/Windows/Avalonia.Win32/Win32Platform.cs +++ b/src/Windows/Avalonia.Win32/Win32Platform.cs @@ -19,6 +19,7 @@ using Avalonia.Threading; using Avalonia.Utilities; using Avalonia.Win32.Input; using Avalonia.Win32.Interop; +using JetBrains.Annotations; using static Avalonia.Win32.Interop.UnmanagedMethods; namespace Avalonia @@ -59,11 +60,6 @@ namespace Avalonia /// GPU rendering will not be enabled if this is set to false. /// public bool? AllowEglInitialization { get; set; } - - public IList EglRendererBlacklist { get; set; } = new List - { - "Microsoft Basic Render" - }; /// /// Embeds popups to the window when set to true. The default value is false. @@ -106,6 +102,11 @@ namespace Avalonia /// which if active will override this setting. /// public bool UseLowLatencyDxgiSwapChain { get; set; } = false; + + /// + /// Provides a way to use a custom-implemented graphics context such as a custom ISkiaGpu + /// + [CanBeNull] public IPlatformGraphics CustomPlatformGraphics { get; set; } } } @@ -139,6 +140,7 @@ namespace Avalonia.Win32 public static Win32PlatformOptions Options { get; private set; } internal static Compositor Compositor { get; private set; } + internal static PlatformRenderInterfaceContextManager RenderInterface { get; private set; } public static void Initialize() { @@ -169,16 +171,19 @@ namespace Avalonia.Win32 .Bind().ToConstant(new NonPumpingSyncContext.HelperImpl()) .Bind().ToConstant(new WindowsMountedVolumeInfoProvider()) .Bind().ToConstant(s_instance); - - var gl = Win32GlManager.Initialize(); - + _uiThread = Thread.CurrentThread; + var platformGraphics = options?.CustomPlatformGraphics + ?? Win32GlManager.Initialize(); + if (OleContext.Current != null) AvaloniaLocator.CurrentMutable.Bind().ToSingleton(); if (Options.UseCompositor) - Compositor = new Compositor(AvaloniaLocator.Current.GetRequiredService(), gl); + Compositor = new Compositor(AvaloniaLocator.Current.GetRequiredService(), platformGraphics); + else + RenderInterface = new PlatformRenderInterfaceContextManager(platformGraphics); } public bool HasMessages() diff --git a/src/Windows/Avalonia.Win32/WinRT/Composition/WinUICompositedWindow.cs b/src/Windows/Avalonia.Win32/WinRT/Composition/WinUICompositedWindow.cs deleted file mode 100644 index ef3de9fbe1..0000000000 --- a/src/Windows/Avalonia.Win32/WinRT/Composition/WinUICompositedWindow.cs +++ /dev/null @@ -1,109 +0,0 @@ -using System; -using System.Numerics; -using System.Reactive.Disposables; -using System.Threading; -using Avalonia.MicroCom; -using Avalonia.OpenGL; -using Avalonia.OpenGL.Egl; -using Avalonia.Win32.Interop; -using MicroCom.Runtime; - -namespace Avalonia.Win32.WinRT.Composition -{ - public class WinUICompositedWindow : IDisposable - { - private EglContext _syncContext; - private readonly object _pumpLock; - private readonly IVisual _micaVisual; - private readonly ICompositionRoundedRectangleGeometry _roundedRectangleGeometry; - private readonly IVisual _blurVisual; - private ICompositionTarget _compositionTarget; - private IVisual _contentVisual; - private ICompositionDrawingSurfaceInterop _surfaceInterop; - private PixelSize _size; - - private static Guid IID_ID3D11Texture2D = Guid.Parse("6f15aaf2-d208-4e89-9ab4-489535d34f9c"); - private ICompositor _compositor; - - - internal WinUICompositedWindow(EglContext syncContext, - ICompositor compositor, - object pumpLock, - ICompositionTarget compositionTarget, - ICompositionDrawingSurfaceInterop surfaceInterop, - IVisual contentVisual, IVisual blurVisual, IVisual micaVisual, - ICompositionRoundedRectangleGeometry roundedRectangleGeometry) - { - _compositor = compositor.CloneReference(); - _syncContext = syncContext; - _pumpLock = pumpLock; - _micaVisual = micaVisual; - _roundedRectangleGeometry = roundedRectangleGeometry; - _blurVisual = blurVisual.CloneReference(); - _compositionTarget = compositionTarget.CloneReference(); - _contentVisual = contentVisual.CloneReference(); - _surfaceInterop = surfaceInterop.CloneReference(); - } - - - public void ResizeIfNeeded(PixelSize size) - { - using (_syncContext.EnsureLocked()) - { - if (_size != size) - { - _surfaceInterop.Resize(new UnmanagedMethods.POINT { X = size.Width, Y = size.Height }); - _contentVisual.SetSize(new Vector2(size.Width, size.Height)); - _roundedRectangleGeometry?.SetSize(new Vector2(size.Width, size.Height)); - _size = size; - } - } - } - - public unsafe IUnknown BeginDrawToTexture(out PixelPoint offset) - { - if (!_syncContext.IsCurrent) - throw new InvalidOperationException(); - - var iid = IID_ID3D11Texture2D; - void* pTexture; - var off = _surfaceInterop.BeginDraw(null, &iid, &pTexture); - offset = new PixelPoint(off.X, off.Y); - return MicroComRuntime.CreateProxyFor(pTexture, true); - } - - public void EndDraw() - { - if (!_syncContext.IsCurrent) - throw new InvalidOperationException(); - _surfaceInterop.EndDraw(); - } - - public void SetBlur(BlurEffect blurEffect) - { - using (_syncContext.EnsureLocked()) - { - _blurVisual.SetIsVisible(blurEffect == BlurEffect.Acrylic ? 1 : 0); - _micaVisual?.SetIsVisible(blurEffect == BlurEffect.Mica ? 1 : 0); - } - } - - public IDisposable BeginTransaction() - { - Monitor.Enter(_pumpLock); - return Disposable.Create(() => Monitor.Exit(_pumpLock)); - } - - public void Dispose() - { - if (_syncContext == null) - { - _compositor.Dispose(); - _blurVisual.Dispose(); - _contentVisual.Dispose(); - _surfaceInterop.Dispose(); - _compositionTarget.Dispose(); - } - } - } -} diff --git a/src/Windows/Avalonia.Win32/WinRT/Composition/WinUICompositorConnection.cs b/src/Windows/Avalonia.Win32/WinRT/Composition/WinUICompositorConnection.cs deleted file mode 100644 index 6da17b8ea5..0000000000 --- a/src/Windows/Avalonia.Win32/WinRT/Composition/WinUICompositorConnection.cs +++ /dev/null @@ -1,311 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Numerics; -using System.Runtime.InteropServices; -using System.Threading; -using System.Threading.Tasks; -using Avalonia.Logging; -using Avalonia.MicroCom; -using Avalonia.OpenGL; -using Avalonia.OpenGL.Angle; -using Avalonia.OpenGL.Egl; -using Avalonia.Rendering; -using Avalonia.Win32.Interop; -using MicroCom.Runtime; - -namespace Avalonia.Win32.WinRT.Composition -{ - class WinUICompositorConnection : IRenderTimer - { - public static readonly Version MinHostBackdropVersion = new Version(10, 0, 22000); - private readonly float? _backdropCornerRadius; - private readonly EglContext _syncContext; - private readonly ICompositionBrush _micaBrush; - private ICompositor _compositor; - private ICompositor5 _compositor5; - private ICompositorInterop _compositorInterop; - private AngleWin32EglDisplay _angle; - private ICompositionGraphicsDevice _device; - private EglPlatformOpenGlInterface _gl; - private ICompositorDesktopInterop _compositorDesktopInterop; - private ICompositionBrush _blurBrush; - private object _pumpLock = new object(); - - public WinUICompositorConnection(EglPlatformOpenGlInterface gl, object pumpLock, float? backdropCornerRadius) - { - _gl = gl; - _pumpLock = pumpLock; - _backdropCornerRadius = backdropCornerRadius; - _syncContext = _gl.PrimaryEglContext; - _angle = (AngleWin32EglDisplay)_gl.Display; - _compositor = NativeWinRTMethods.CreateInstance("Windows.UI.Composition.Compositor"); - _compositor5 = _compositor.QueryInterface(); - _compositorInterop = _compositor.QueryInterface(); - _compositorDesktopInterop = _compositor.QueryInterface(); - using var device = MicroComRuntime.CreateProxyFor(_angle.GetDirect3DDevice(), true); - - _device = _compositorInterop.CreateGraphicsDevice(device); - _blurBrush = CreateAcrylicBlurBackdropBrush(); - _micaBrush = CreateMicaBackdropBrush(); - } - - public EglPlatformOpenGlInterface Egl => _gl; - - static bool TryCreateAndRegisterCore(EglPlatformOpenGlInterface angle, float? backdropCornerRadius) - { - var tcs = new TaskCompletionSource(); - var pumpLock = new object(); - var th = new Thread(() => - { - WinUICompositorConnection connect; - try - { - NativeWinRTMethods.CreateDispatcherQueueController(new NativeWinRTMethods.DispatcherQueueOptions - { - apartmentType = NativeWinRTMethods.DISPATCHERQUEUE_THREAD_APARTMENTTYPE.DQTAT_COM_NONE, - dwSize = Marshal.SizeOf(), - threadType = NativeWinRTMethods.DISPATCHERQUEUE_THREAD_TYPE.DQTYPE_THREAD_CURRENT - }); - connect = new WinUICompositorConnection(angle, pumpLock, backdropCornerRadius); - AvaloniaLocator.CurrentMutable.BindToSelf(connect); - AvaloniaLocator.CurrentMutable.Bind().ToConstant(connect); - tcs.SetResult(true); - - } - catch (Exception e) - { - tcs.SetException(e); - return; - } - connect.RunLoop(); - }) - { - IsBackground = true - }; - th.SetApartmentState(ApartmentState.STA); - th.Start(); - return tcs.Task.Result; - } - - class RunLoopHandler : IAsyncActionCompletedHandler, IMicroComShadowContainer - { - private readonly WinUICompositorConnection _parent; - private Stopwatch _st = Stopwatch.StartNew(); - - public RunLoopHandler(WinUICompositorConnection parent) - { - _parent = parent; - } - public void Dispose() - { - - } - - public void Invoke(IAsyncAction asyncInfo, AsyncStatus asyncStatus) - { - _parent.Tick?.Invoke(_st.Elapsed); - using var act = _parent._compositor5.RequestCommitAsync(); - act.SetCompleted(this); - } - - public MicroComShadow Shadow { get; set; } - public void OnReferencedFromNative() - { - } - - public void OnUnreferencedFromNative() - { - } - } - - private void RunLoop() - { - var cts = new CancellationTokenSource(); - AppDomain.CurrentDomain.ProcessExit += (sender, args) => - cts.Cancel(); - - using (var act = _compositor5.RequestCommitAsync()) - act.SetCompleted(new RunLoopHandler(this)); - - while (!cts.IsCancellationRequested) - { - UnmanagedMethods.GetMessage(out var msg, IntPtr.Zero, 0, 0); - lock (_pumpLock) - UnmanagedMethods.DispatchMessage(ref msg); - } - } - - public static void TryCreateAndRegister(EglPlatformOpenGlInterface angle, - float? backdropCornerRadius) - { - const int majorRequired = 10; - const int buildRequired = 17134; - - var majorInstalled = Win32Platform.WindowsVersion.Major; - var buildInstalled = Win32Platform.WindowsVersion.Build; - - if (majorInstalled >= majorRequired && - buildInstalled >= buildRequired) - { - try - { - TryCreateAndRegisterCore(angle, backdropCornerRadius); - return; - } - catch (Exception e) - { - Logger.TryGet(LogEventLevel.Error, "WinUIComposition") - ?.Log(null, "Unable to initialize WinUI compositor: {0}", e); - - } - } - - var osVersionNotice = - $"Windows {majorRequired} Build {buildRequired} is required. Your machine has Windows {majorInstalled} Build {buildInstalled} installed."; - - Logger.TryGet(LogEventLevel.Warning, "WinUIComposition")?.Log(null, - $"Unable to initialize WinUI compositor: {osVersionNotice}"); - } - - - public WinUICompositedWindow CreateWindow(IntPtr hWnd) - { - using var sc = _syncContext.EnsureLocked(); - using var desktopTarget = _compositorDesktopInterop.CreateDesktopWindowTarget(hWnd, 0); - using var target = desktopTarget.QueryInterface(); - using var device2 = _device.QueryInterface(); - - using var drawingSurface = device2.CreateDrawingSurface2(new UnmanagedMethods.SIZE(), DirectXPixelFormat.B8G8R8A8UIntNormalized, - DirectXAlphaMode.Premultiplied); - using var surface = drawingSurface.QueryInterface(); - using var surfaceInterop = drawingSurface.QueryInterface(); - - using var surfaceBrush = _compositor.CreateSurfaceBrushWithSurface(surface); - using var brush = surfaceBrush.QueryInterface(); - - using var spriteVisual = _compositor.CreateSpriteVisual(); - spriteVisual.SetBrush(brush); - using var visual = spriteVisual.QueryInterface(); - using var visual2 = spriteVisual.QueryInterface(); - using var container = _compositor.CreateContainerVisual(); - using var containerVisual = container.QueryInterface(); - using var containerVisual2 = container.QueryInterface(); - containerVisual2.SetRelativeSizeAdjustment(new Vector2(1, 1)); - using var containerChildren = container.Children; - - target.SetRoot(containerVisual); - - using var blur = CreateBlurVisual(_blurBrush); - IVisual mica = null; - if (_micaBrush != null) - { - mica = CreateBlurVisual(_micaBrush); - containerChildren.InsertAtTop(mica); - } - - var compositionRoundedRectangleGeometry = ClipVisual(blur, mica); - - containerChildren.InsertAtTop(blur); - containerChildren.InsertAtTop(visual); - - return new WinUICompositedWindow(_syncContext, _compositor, _pumpLock, target, surfaceInterop, visual, - blur, mica, compositionRoundedRectangleGeometry); - } - - private ICompositionBrush CreateMicaBackdropBrush() - { - if (Win32Platform.WindowsVersion.Build < 22000) - return null; - - using var compositorWithBlurredWallpaperBackdropBrush = - _compositor.QueryInterface(); - using var blurredWallpaperBackdropBrush = - compositorWithBlurredWallpaperBackdropBrush?.TryCreateBlurredWallpaperBackdropBrush(); - using var micaBackdropBrush = blurredWallpaperBackdropBrush?.QueryInterface(); - return micaBackdropBrush.CloneReference(); - } - - private unsafe ICompositionBrush CreateAcrylicBlurBackdropBrush() - { - using var backDropParameterFactory = NativeWinRTMethods.CreateActivationFactory( - "Windows.UI.Composition.CompositionEffectSourceParameter"); - using var backdropString = new HStringInterop("backdrop"); - using var backDropParameter = - backDropParameterFactory.Create(backdropString.Handle); - using var backDropParameterAsSource = backDropParameter.QueryInterface(); - var blurEffect = new WinUIGaussianBlurEffect(backDropParameterAsSource); - using var blurEffectFactory = _compositor.CreateEffectFactory(blurEffect); - using var compositionEffectBrush = blurEffectFactory.CreateBrush(); - using var backdropBrush = CreateBackdropBrush(); - - var saturateEffect = new SaturationEffect(blurEffect); - using var satEffectFactory = _compositor.CreateEffectFactory(saturateEffect); - using var sat = satEffectFactory.CreateBrush(); - compositionEffectBrush.SetSourceParameter(backdropString.Handle, backdropBrush); - return compositionEffectBrush.QueryInterface(); - } - - private ICompositionRoundedRectangleGeometry ClipVisual(params IVisual[] containerVisuals) - { - if (!_backdropCornerRadius.HasValue) - return null; - using var roundedRectangleGeometry = _compositor5.CreateRoundedRectangleGeometry(); - roundedRectangleGeometry.SetCornerRadius(new Vector2(_backdropCornerRadius.Value, _backdropCornerRadius.Value)); - - using var compositor6 = _compositor.QueryInterface(); - using var compositionGeometry = roundedRectangleGeometry - .QueryInterface(); - - using var geometricClipWithGeometry = - compositor6.CreateGeometricClipWithGeometry(compositionGeometry); - foreach (var visual in containerVisuals) - { - visual?.SetClip(geometricClipWithGeometry.QueryInterface()); - } - - return roundedRectangleGeometry.CloneReference(); - } - - private unsafe IVisual CreateBlurVisual(ICompositionBrush compositionBrush) - { - using var spriteVisual = _compositor.CreateSpriteVisual(); - using var visual = spriteVisual.QueryInterface(); - using var visual2 = spriteVisual.QueryInterface(); - - - spriteVisual.SetBrush(compositionBrush); - visual.SetIsVisible(0); - visual2.SetRelativeSizeAdjustment(new Vector2(1.0f, 1.0f)); - - return visual.CloneReference(); - } - - private ICompositionBrush CreateBackdropBrush() - { - ICompositionBackdropBrush brush = null; - try - { - if (Win32Platform.WindowsVersion >= MinHostBackdropVersion) - { - using var compositor3 = _compositor.QueryInterface(); - brush = compositor3.CreateHostBackdropBrush(); - } - else - { - using var compositor2 = _compositor.QueryInterface(); - brush = compositor2.CreateBackdropBrush(); - } - - return brush.QueryInterface(); - } - finally - { - brush?.Dispose(); - } - } - - public event Action Tick; - public bool RunsInBackground => true; - } -} diff --git a/src/Windows/Avalonia.Win32/WinRT/Composition/WinUiCompositedWindow.cs b/src/Windows/Avalonia.Win32/WinRT/Composition/WinUiCompositedWindow.cs new file mode 100644 index 0000000000..32019f4c15 --- /dev/null +++ b/src/Windows/Avalonia.Win32/WinRT/Composition/WinUiCompositedWindow.cs @@ -0,0 +1,110 @@ +using System; +using System.Numerics; +using System.Reactive.Disposables; +using System.Threading; +using Avalonia.OpenGL.Egl; +using Avalonia.Win32.Interop; +using MicroCom.Runtime; + +namespace Avalonia.Win32.WinRT.Composition; + +internal class WinUiCompositedWindow : IDisposable +{ + public EglGlPlatformSurface.IEglWindowGlPlatformSurfaceInfo WindowInfo { get; } + private readonly WinUiCompositionShared _shared; + private readonly ICompositionRoundedRectangleGeometry _compositionRoundedRectangleGeometry; + private readonly IVisual _mica; + private readonly IVisual _blur; + private readonly IVisual _visual; + private PixelSize _size; + private readonly ICompositionSurfaceBrush _surfaceBrush; + private readonly ICompositionTarget _target; + + public void Dispose() + { + lock (_shared.SyncRoot) + { + _compositionRoundedRectangleGeometry?.Dispose(); + _blur?.Dispose(); + _mica?.Dispose(); + _visual.Dispose(); + _surfaceBrush.Dispose(); + _target.Dispose(); + } + } + + public WinUiCompositedWindow(EglGlPlatformSurface.IEglWindowGlPlatformSurfaceInfo info, + WinUiCompositionShared shared, float? backdropCornerRadius) + { + WindowInfo = info; + _shared = shared; + using var desktopTarget = shared.DesktopInterop.CreateDesktopWindowTarget(WindowInfo.Handle, 0); + _target = desktopTarget.QueryInterface(); + + + using var container = shared.Compositor.CreateContainerVisual(); + using var containerVisual = container.QueryInterface(); + using var containerVisual2 = container.QueryInterface(); + containerVisual2.SetRelativeSizeAdjustment(new Vector2(1, 1)); + using var containerChildren = container.Children; + + _target.SetRoot(containerVisual); + + _blur = WinUiCompositionUtils.CreateBlurVisual(shared.Compositor, shared.BlurBrush); + if (shared.MicaBrush != null) + { + _mica = WinUiCompositionUtils.CreateBlurVisual(shared.Compositor, shared.MicaBrush); + containerChildren.InsertAtTop(_mica); + } + + _compositionRoundedRectangleGeometry = + WinUiCompositionUtils.ClipVisual(shared.Compositor, backdropCornerRadius, _blur, _mica); + + containerChildren.InsertAtTop(_blur); + using var spriteVisual = shared.Compositor.CreateSpriteVisual(); + _visual = spriteVisual.QueryInterface(); + containerChildren.InsertAtTop(_visual); + + _surfaceBrush = shared.Compositor.CreateSurfaceBrush(); + using var compositionBrush = _surfaceBrush.QueryInterface(); + spriteVisual.SetBrush(compositionBrush); + _target.SetRoot(containerVisual); + + + + } + + public void SetSurface(ICompositionSurface surface) => _surfaceBrush.SetSurface(surface); + + public void SetBlur(BlurEffect blurEffect) + { + lock (_shared.SyncRoot) + { + + _blur.SetIsVisible(blurEffect == BlurEffect.Acrylic + || blurEffect == BlurEffect.Mica && _mica == null ? + 1 : + 0); + _mica?.SetIsVisible(blurEffect == BlurEffect.Mica ? 1 : 0); + } + } + + public IDisposable BeginTransaction() + { + Monitor.Enter(_shared.SyncRoot); + return Disposable.Create(() => Monitor.Exit(_shared.SyncRoot)); + } + + public void ResizeIfNeeded(PixelSize size) + { + lock (_shared.SyncRoot) + { + if (_size != size) + { + _visual.SetSize(new Vector2(size.Width, size.Height)); + _compositionRoundedRectangleGeometry?.SetSize(new Vector2(size.Width, size.Height)); + _size = size; + } + } + } +} diff --git a/src/Windows/Avalonia.Win32/WinRT/Composition/WinUiCompositedWindowSurface.cs b/src/Windows/Avalonia.Win32/WinRT/Composition/WinUiCompositedWindowSurface.cs index fc04dcda26..9ee024f9ad 100644 --- a/src/Windows/Avalonia.Win32/WinRT/Composition/WinUiCompositedWindowSurface.cs +++ b/src/Windows/Avalonia.Win32/WinRT/Composition/WinUiCompositedWindowSurface.cs @@ -1,119 +1,203 @@ using System; using System.Runtime.InteropServices; using Avalonia.MicroCom; +using Avalonia.OpenGL; using Avalonia.OpenGL.Angle; using Avalonia.OpenGL.Egl; using Avalonia.OpenGL.Surfaces; +using Avalonia.Platform; using Avalonia.Utilities; +using Avalonia.Win32.DirectX; using Avalonia.Win32.Interop; +using Avalonia.Win32.OpenGl.Angle; using MicroCom.Runtime; namespace Avalonia.Win32.WinRT.Composition { - internal class WinUiCompositedWindowSurface : EglGlPlatformSurfaceBase, IBlurHost, IDisposable + internal class WinUiCompositedWindowSurface : IDirect3D11TexturePlatformSurface, IDisposable, IBlurHost { - private readonly WinUICompositorConnection _connection; - private EglPlatformOpenGlInterface _egl; - private readonly EglGlPlatformSurfaceBase.IEglWindowGlPlatformSurfaceInfo _info; - private IRef _window; + private readonly WinUiCompositionShared _shared; + private readonly EglGlPlatformSurface.IEglWindowGlPlatformSurfaceInfo _info; + private WinUiCompositedWindow _window; private BlurEffect _blurEffect; - public WinUiCompositedWindowSurface(WinUICompositorConnection connection, IEglWindowGlPlatformSurfaceInfo info) : base() + public WinUiCompositedWindowSurface(WinUiCompositionShared shared, EglGlPlatformSurface.IEglWindowGlPlatformSurfaceInfo info) { - _connection = connection; - _egl = connection.Egl; + _shared = shared; _info = info; } - public override IGlPlatformSurfaceRenderTarget CreateGlRenderTarget() + public IDirect3D11TextureRenderTarget CreateRenderTarget(IPlatformGraphicsContext context, IntPtr d3dDevice) { - using (_egl.PrimaryContext.EnsureCurrent()) - { - if (_window?.Item == null) - { - _window = RefCountable.Create(_connection.CreateWindow(_info.Handle)); - _window.Item.SetBlur(_blurEffect); - } + var cornerRadius = AvaloniaLocator.Current.GetService() + ?.CompositionBackdropCornerRadius; + _window ??= new WinUiCompositedWindow(_info, _shared, cornerRadius); + _window.SetBlur(_blurEffect); - return new CompositionRenderTarget(_egl, _window, _info); - } + return new WinUiCompositedWindowRenderTarget(context, _window, d3dDevice, _shared.Compositor); + } + + public void Dispose() + { + _window?.Dispose(); + _window = null; + } + + public void SetBlur(BlurEffect enable) + { + _blurEffect = enable; + _window?.SetBlur(enable); } + } - class CompositionRenderTarget : EglPlatformSurfaceRenderTargetBase + internal class WinUiCompositedWindowRenderTarget : IDirect3D11TextureRenderTarget + { + private readonly IPlatformGraphicsContext _context; + private readonly WinUiCompositedWindow _window; + private readonly IUnknown _d3dDevice; + private readonly ICompositor _compositor; + private readonly ICompositorInterop _interop; + private readonly ICompositionGraphicsDevice _compositionDevice; + private readonly ICompositionGraphicsDevice2 _compositionDevice2; + private readonly ICompositionSurface _surface; + private PixelSize _size; + private bool _lost; + private readonly ICompositionDrawingSurfaceInterop _surfaceInterop; + + public WinUiCompositedWindowRenderTarget(IPlatformGraphicsContext context, + WinUiCompositedWindow window, IntPtr device, + ICompositor compositor) { - private readonly EglPlatformOpenGlInterface _egl; - private readonly IRef _window; - private readonly IEglWindowGlPlatformSurfaceInfo _info; - - public CompositionRenderTarget(EglPlatformOpenGlInterface egl, - IRef window, - IEglWindowGlPlatformSurfaceInfo info) - : base(egl) + _context = context; + _window = window; + + try { - _egl = egl; - _window = window.Clone(); - _info = info; - _window.Item.ResizeIfNeeded(_info.Size); + _d3dDevice = MicroComRuntime.CreateProxyFor(device, false).CloneReference(); + _compositor = compositor.CloneReference(); + _interop = compositor.QueryInterface(); + _compositionDevice = _interop.CreateGraphicsDevice(_d3dDevice); + _compositionDevice2 = _compositionDevice.QueryInterface(); + _drawingSurface = _compositionDevice2.CreateDrawingSurface2(new UnmanagedMethods.SIZE(), + DirectXPixelFormat.B8G8R8A8UIntNormalized, DirectXAlphaMode.Premultiplied); + _surface = _drawingSurface.QueryInterface(); + _surfaceInterop = _drawingSurface.QueryInterface(); } + finally + { + if (_surfaceInterop == null) + Dispose(); + } + } - public override IGlPlatformSurfaceRenderingSession BeginDraw() + public void Dispose() + { + _surface?.Dispose(); + _surfaceInterop?.Dispose(); + _drawingSurface?.Dispose(); + _compositionDevice2?.Dispose(); + _compositionDevice?.Dispose(); + _interop?.Dispose(); + _compositor?.Dispose(); + _d3dDevice?.Dispose(); + } + + public bool IsCorrupted => _context.IsLost || _lost; + private static Guid IID_ID3D11Texture2D = Guid.Parse("6f15aaf2-d208-4e89-9ab4-489535d34f9c"); + private readonly ICompositionDrawingSurface _drawingSurface; + + public unsafe IDirect3D11TextureRenderTargetRenderSession BeginDraw() + { + if (IsCorrupted) + throw new RenderTargetCorruptedException(); + var transaction = _window.BeginTransaction(); + bool needsEndDraw = false; + try { - var contextLock = _egl.PrimaryEglContext.EnsureCurrent(); - IUnknown texture = null; - EglSurface surface = null; - IDisposable transaction = null; - var success = false; + var size = _window.WindowInfo.Size; + var scale = _window.WindowInfo.Scaling; + _window.ResizeIfNeeded(size); + _window.SetSurface(_surface); + + void* pTexture; + UnmanagedMethods.POINT off; try { - if (_window?.Item == null) - throw new ObjectDisposedException(GetType().FullName); - - var size = _info.Size; - transaction = _window.Item.BeginTransaction(); - _window.Item.ResizeIfNeeded(size); - texture = _window.Item.BeginDrawToTexture(out var offset); - - surface = ((AngleWin32EglDisplay) _egl.Display).WrapDirect3D11Texture(_egl, - texture.GetNativeIntPtr(), - offset.X, offset.Y, size.Width, size.Height); - - var res = base.BeginDraw(surface, _info, () => + if (_size != size) { - surface?.Dispose(); - texture?.Dispose(); - _window.Item.EndDraw(); - transaction?.Dispose(); - contextLock?.Dispose(); - }, true); - success = true; - return res; + _surfaceInterop.Resize(new UnmanagedMethods.POINT + { + X = size.Width, + Y = size.Height + }); + _size = size; + } + var iid = IID_ID3D11Texture2D; + off = _surfaceInterop.BeginDraw(null, &iid, &pTexture); } - finally + catch (Exception e) { - if (!success) - { - surface?.Dispose(); - texture?.Dispose(); - transaction?.Dispose(); - contextLock.Dispose(); - } + _lost = true; + throw new RenderTargetCorruptedException(e); + } + + needsEndDraw = true; + var offset = new PixelPoint(off.X, off.Y); + using var texture = MicroComRuntime.CreateProxyFor(pTexture, true); + + var session = new Session(_surfaceInterop, texture, transaction, _size, offset, scale); + transaction = null; + return session; + } + finally + { + if (transaction != null) + { + if (needsEndDraw) + _surfaceInterop.EndDraw(); + transaction?.Dispose(); } } } - public void SetBlur(BlurEffect blurEffect) + class Session : IDirect3D11TextureRenderTargetRenderSession { - _blurEffect = blurEffect; - _window?.Item?.SetBlur(blurEffect); - } + private readonly IDisposable _transaction; + private readonly PixelSize _size; + private readonly PixelPoint _offset; + private readonly double _scaling; + private readonly ICompositionDrawingSurfaceInterop _surfaceInterop; + private readonly IUnknown _texture; - public void Dispose() - { - using (_egl.PrimaryEglContext.EnsureLocked()) + public Session(ICompositionDrawingSurfaceInterop surfaceInterop, IUnknown texture, IDisposable transaction, + PixelSize size, PixelPoint offset, double scaling) { - _window?.Dispose(); - _window = null; + _transaction = transaction; + _size = size; + _offset = offset; + _scaling = scaling; + _surfaceInterop = surfaceInterop.CloneReference(); + _texture = texture.CloneReference(); } + + public void Dispose() + { + try + { + _texture.Dispose(); + _surfaceInterop.EndDraw(); + _surfaceInterop.Dispose(); + } + finally + { + _transaction.Dispose(); + } + } + + public IntPtr D3D11Texture2D => _texture.GetNativeIntPtr(); + public PixelSize Size => _size; + public PixelPoint Offset => _offset; + public double Scaling => _scaling; } } -} +} \ No newline at end of file diff --git a/src/Windows/Avalonia.Win32/WinRT/Composition/WinUiCompositionShared.cs b/src/Windows/Avalonia.Win32/WinRT/Composition/WinUiCompositionShared.cs new file mode 100644 index 0000000000..301e605e7a --- /dev/null +++ b/src/Windows/Avalonia.Win32/WinRT/Composition/WinUiCompositionShared.cs @@ -0,0 +1,34 @@ +using System; +using MicroCom.Runtime; + +namespace Avalonia.Win32.WinRT.Composition; + +internal class WinUiCompositionShared : IDisposable +{ + public ICompositor Compositor { get; } + public ICompositor5 Compositor5 { get; } + public ICompositorDesktopInterop DesktopInterop { get; } + public ICompositionBrush BlurBrush; + public ICompositionBrush MicaBrush; + public object SyncRoot { get; } = new(); + + public static readonly Version MinHostBackdropVersion = new Version(10, 0, 22000); + + public WinUiCompositionShared(ICompositor compositor) + { + Compositor = compositor.CloneReference(); + Compositor5 = compositor.QueryInterface(); + BlurBrush = WinUiCompositionUtils.CreateAcrylicBlurBackdropBrush(compositor); + MicaBrush = WinUiCompositionUtils.CreateMicaBackdropBrush(compositor); + DesktopInterop = compositor.QueryInterface(); + } + + public void Dispose() + { + BlurBrush.Dispose(); + MicaBrush.Dispose(); + DesktopInterop.Dispose(); + Compositor.Dispose(); + Compositor5.Dispose(); + } +} \ No newline at end of file diff --git a/src/Windows/Avalonia.Win32/WinRT/Composition/WinUiCompositionUtils.cs b/src/Windows/Avalonia.Win32/WinRT/Composition/WinUiCompositionUtils.cs new file mode 100644 index 0000000000..e7af4046d6 --- /dev/null +++ b/src/Windows/Avalonia.Win32/WinRT/Composition/WinUiCompositionUtils.cs @@ -0,0 +1,101 @@ +using System; +using System.Numerics; +using MicroCom.Runtime; + +namespace Avalonia.Win32.WinRT.Composition; + +internal static class WinUiCompositionUtils +{ + public static ICompositionBrush CreateMicaBackdropBrush(ICompositor compositor) + { + if (Win32Platform.WindowsVersion.Build < 22000) + return null; + + using var compositorWithBlurredWallpaperBackdropBrush = + compositor.QueryInterface(); + using var blurredWallpaperBackdropBrush = + compositorWithBlurredWallpaperBackdropBrush?.TryCreateBlurredWallpaperBackdropBrush(); + return blurredWallpaperBackdropBrush?.QueryInterface(); + } + + public static unsafe ICompositionBrush CreateAcrylicBlurBackdropBrush(ICompositor compositor) + { + using var backDropParameterFactory = + NativeWinRTMethods.CreateActivationFactory( + "Windows.UI.Composition.CompositionEffectSourceParameter"); + using var backdropString = new HStringInterop("backdrop"); + using var backDropParameter = + backDropParameterFactory.Create(backdropString.Handle); + using var backDropParameterAsSource = backDropParameter.QueryInterface(); + var blurEffect = new WinUIGaussianBlurEffect(backDropParameterAsSource); + using var blurEffectFactory = compositor.CreateEffectFactory(blurEffect); + using var compositionEffectBrush = blurEffectFactory.CreateBrush(); + using var backdropBrush = CreateBackdropBrush(compositor); + + var saturateEffect = new SaturationEffect(blurEffect); + using var satEffectFactory = compositor.CreateEffectFactory(saturateEffect); + using var sat = satEffectFactory.CreateBrush(); + compositionEffectBrush.SetSourceParameter(backdropString.Handle, backdropBrush); + return compositionEffectBrush.QueryInterface(); + } + + public static ICompositionRoundedRectangleGeometry ClipVisual(ICompositor compositor, float? _backdropCornerRadius, params IVisual[] containerVisuals) + { + if (!_backdropCornerRadius.HasValue) + return null; + using var compositor5 = compositor.QueryInterface(); + using var roundedRectangleGeometry = compositor5.CreateRoundedRectangleGeometry(); + roundedRectangleGeometry.SetCornerRadius(new Vector2(_backdropCornerRadius.Value, _backdropCornerRadius.Value)); + + using var compositor6 = compositor.QueryInterface(); + using var compositionGeometry = roundedRectangleGeometry + .QueryInterface(); + + using var geometricClipWithGeometry = + compositor6.CreateGeometricClipWithGeometry(compositionGeometry); + foreach (var visual in containerVisuals) + { + visual?.SetClip(geometricClipWithGeometry.QueryInterface()); + } + + return roundedRectangleGeometry.CloneReference(); + } + + public static IVisual CreateBlurVisual(ICompositor compositor, ICompositionBrush compositionBrush) + { + using var spriteVisual = compositor.CreateSpriteVisual(); + using var visual = spriteVisual.QueryInterface(); + using var visual2 = spriteVisual.QueryInterface(); + + + spriteVisual.SetBrush(compositionBrush); + visual.SetIsVisible(0); + visual2.SetRelativeSizeAdjustment(new Vector2(1.0f, 1.0f)); + + return visual.CloneReference(); + } + + public static ICompositionBrush CreateBackdropBrush(ICompositor compositor) + { + ICompositionBackdropBrush brush = null; + try + { + if (Win32Platform.WindowsVersion >= WinUiCompositionShared.MinHostBackdropVersion) + { + using var compositor3 = compositor.QueryInterface(); + brush = compositor3.CreateHostBackdropBrush(); + } + else + { + using var compositor2 = compositor.QueryInterface(); + brush = compositor2.CreateBackdropBrush(); + } + + return brush.QueryInterface(); + } + finally + { + brush?.Dispose(); + } + } +} \ No newline at end of file diff --git a/src/Windows/Avalonia.Win32/WinRT/Composition/WinUiCompositorConnection.cs b/src/Windows/Avalonia.Win32/WinRT/Composition/WinUiCompositorConnection.cs new file mode 100644 index 0000000000..a1408baae0 --- /dev/null +++ b/src/Windows/Avalonia.Win32/WinRT/Composition/WinUiCompositorConnection.cs @@ -0,0 +1,151 @@ +using System; +using System.Diagnostics; +using System.Runtime.InteropServices; +using System.Threading; +using System.Threading.Tasks; +using Avalonia.Logging; +using Avalonia.MicroCom; +using Avalonia.OpenGL.Egl; +using Avalonia.Rendering; +using Avalonia.Win32.DirectX; +using Avalonia.Win32.Interop; +using Avalonia.Win32.OpenGl.Angle; +using MicroCom.Runtime; + +namespace Avalonia.Win32.WinRT.Composition; + +internal class WinUiCompositorConnection : IRenderTimer +{ + private readonly WinUiCompositionShared _shared; + public event Action Tick; + public bool RunsInBackground => true; + + public unsafe WinUiCompositorConnection() + { + using var compositor = NativeWinRTMethods.CreateInstance("Windows.UI.Composition.Compositor"); + /* + var levels = new[] { D3D_FEATURE_LEVEL.D3D_FEATURE_LEVEL_11_1 }; + DirectXUnmanagedMethods.D3D11CreateDevice(IntPtr.Zero, D3D_DRIVER_TYPE.D3D_DRIVER_TYPE_HARDWARE, + IntPtr.Zero, 0, levels, (uint)levels.Length, 7, out var pD3dDevice, out var level, null); + + var d3dDevice = MicroComRuntime.CreateProxyFor(pD3dDevice, true); + + var compositionDevice = compositor.QueryInterface().CreateGraphicsDevice(d3dDevice); + var surf = compositionDevice.CreateDrawingSurface(new UnmanagedMethods.SIZE_F { X = 100, Y = 100 }, + DirectXPixelFormat.B8G8R8A8UIntNormalized, DirectXAlphaMode.Premultiplied); + var surfInterop = surf.QueryInterface(); + var IID_ID3D11Texture2D = Guid.Parse("6f15aaf2-d208-4e89-9ab4-489535d34f9c"); + void* texture = null; + surfInterop.BeginDraw(null, &IID_ID3D11Texture2D, &texture); + */ + + _shared = new WinUiCompositionShared(compositor); + } + + static bool TryCreateAndRegisterCore() + { + var tcs = new TaskCompletionSource(); + var pumpLock = new object(); + var th = new Thread(() => + { + WinUiCompositorConnection connect; + try + { + NativeWinRTMethods.CreateDispatcherQueueController(new NativeWinRTMethods.DispatcherQueueOptions + { + apartmentType = NativeWinRTMethods.DISPATCHERQUEUE_THREAD_APARTMENTTYPE.DQTAT_COM_NONE, + dwSize = Marshal.SizeOf(), + threadType = NativeWinRTMethods.DISPATCHERQUEUE_THREAD_TYPE.DQTYPE_THREAD_CURRENT + }); + connect = new WinUiCompositorConnection(); + AvaloniaLocator.CurrentMutable.BindToSelf(connect); + AvaloniaLocator.CurrentMutable.Bind().ToConstant(connect); + tcs.SetResult(true); + + } + catch (Exception e) + { + tcs.SetException(e); + return; + } + + connect.RunLoop(); + }) + { + IsBackground = true, + Name = "DwmRenderTimerLoop" + }; + th.SetApartmentState(ApartmentState.STA); + th.Start(); + return tcs.Task.Result; + } + + class RunLoopHandler : CallbackBase, IAsyncActionCompletedHandler + { + private readonly WinUiCompositorConnection _parent; + private Stopwatch _st = Stopwatch.StartNew(); + + public RunLoopHandler(WinUiCompositorConnection parent) + { + _parent = parent; + } + + public void Invoke(IAsyncAction asyncInfo, AsyncStatus asyncStatus) + { + _parent.Tick?.Invoke(_st.Elapsed); + using var act = _parent._shared.Compositor5.RequestCommitAsync(); + act.SetCompleted(this); + } + } + + private void RunLoop() + { + var cts = new CancellationTokenSource(); + AppDomain.CurrentDomain.ProcessExit += (sender, args) => + cts.Cancel(); + + lock (_shared.SyncRoot) + using (var act = _shared.Compositor5.RequestCommitAsync()) + act.SetCompleted(new RunLoopHandler(this)); + + while (!cts.IsCancellationRequested) + { + UnmanagedMethods.GetMessage(out var msg, IntPtr.Zero, 0, 0); + lock (_shared.SyncRoot) + UnmanagedMethods.DispatchMessage(ref msg); + } + } + + public static void TryCreateAndRegister() + { + const int majorRequired = 10; + const int buildRequired = 17134; + + var majorInstalled = Win32Platform.WindowsVersion.Major; + var buildInstalled = Win32Platform.WindowsVersion.Build; + + if (majorInstalled >= majorRequired && + buildInstalled >= buildRequired) + { + try + { + TryCreateAndRegisterCore(); + return; + } + catch (Exception e) + { + Logger.TryGet(LogEventLevel.Error, "WinUIComposition") + ?.Log(null, "Unable to initialize WinUI compositor: {0}", e); + + } + } + + var osVersionNotice = + $"Windows {majorRequired} Build {buildRequired} is required. Your machine has Windows {majorInstalled} Build {buildInstalled} installed."; + + Logger.TryGet(LogEventLevel.Warning, "WinUIComposition")?.Log(null, + $"Unable to initialize WinUI compositor: {osVersionNotice}"); + } + + public WinUiCompositedWindowSurface CreateSurface(EglGlPlatformSurface.IEglWindowGlPlatformSurfaceInfo info) => new(_shared, info); +} diff --git a/src/Windows/Avalonia.Win32/WinRT/WinRTColor.cs b/src/Windows/Avalonia.Win32/WinRT/WinRTColor.cs index 786d698daa..35f0737d5f 100644 --- a/src/Windows/Avalonia.Win32/WinRT/WinRTColor.cs +++ b/src/Windows/Avalonia.Win32/WinRT/WinRTColor.cs @@ -3,7 +3,7 @@ namespace Avalonia.Win32.WinRT { [StructLayout(LayoutKind.Sequential, Pack = 1)] - public struct WinRTColor + public record struct WinRTColor { public byte A; public byte R; diff --git a/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs b/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs index 5537a0b704..fe7d881f11 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs @@ -134,13 +134,18 @@ namespace Avalonia.Win32 case WindowsMessage.WM_KEYDOWN: case WindowsMessage.WM_SYSKEYDOWN: { - e = new RawKeyEventArgs( - WindowsKeyboardDevice.Instance, - timestamp, - _owner, - RawKeyEventType.KeyDown, - KeyInterop.KeyFromVirtualKey(ToInt32(wParam), ToInt32(lParam)), - WindowsKeyboardDevice.Instance.Modifiers); + var key = KeyInterop.KeyFromVirtualKey(ToInt32(wParam), ToInt32(lParam)); + + if (key != Key.None) + { + e = new RawKeyEventArgs( + WindowsKeyboardDevice.Instance, + timestamp, + _owner, + RawKeyEventType.KeyDown, + key, + WindowsKeyboardDevice.Instance.Modifiers); + } break; } @@ -159,13 +164,18 @@ namespace Avalonia.Win32 case WindowsMessage.WM_KEYUP: case WindowsMessage.WM_SYSKEYUP: { - e = new RawKeyEventArgs( + var key = KeyInterop.KeyFromVirtualKey(ToInt32(wParam), ToInt32(lParam)); + + if (key != Key.None) + { + e = new RawKeyEventArgs( WindowsKeyboardDevice.Instance, timestamp, _owner, RawKeyEventType.KeyUp, - KeyInterop.KeyFromVirtualKey(ToInt32(wParam), ToInt32(lParam)), + key, WindowsKeyboardDevice.Instance.Modifiers); + } break; } case WindowsMessage.WM_CHAR: diff --git a/src/Windows/Avalonia.Win32/WindowImpl.cs b/src/Windows/Avalonia.Win32/WindowImpl.cs index 5f813eec1d..9e11959101 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.cs @@ -26,7 +26,8 @@ using static Avalonia.Win32.Interop.UnmanagedMethods; using Avalonia.Collections.Pooled; using Avalonia.Metadata; using Avalonia.Platform.Storage; -using Avalonia.Win32.DxgiSwapchain; +using Avalonia.Win32.DirectX; +using Avalonia.Win32.OpenGl.Angle; namespace Avalonia.Win32 { @@ -64,7 +65,6 @@ namespace Avalonia.Win32 private Thickness _offScreenMargin; private double _extendTitleBarHint = -1; private bool _isUsingComposition; - private bool _isUsingDxgiSwapchain; private IBlurHost _blurHost; private PlatformResizeReason _resizeReason; private MOUSEMOVEPOINT _lastWmMousePoint; @@ -79,7 +79,7 @@ namespace Avalonia.Win32 private readonly PenDevice _penDevice; private readonly ManagedDeferredRendererLock _rendererLock; private readonly FramebufferManager _framebuffer; - private readonly IGlPlatformSurface _gl; + private readonly object _gl; private readonly bool _wmPointerEnabled; private Win32NativeControlHost _nativeControlHost; @@ -136,23 +136,20 @@ namespace Avalonia.Win32 }; _rendererLock = new ManagedDeferredRendererLock(); - var glPlatform = AvaloniaLocator.Current.GetService(); + var glPlatform = AvaloniaLocator.Current.GetService(); - var compositionConnector = AvaloniaLocator.Current.GetService(); + var compositionConnector = AvaloniaLocator.Current.GetService(); - _isUsingComposition = compositionConnector is { } && - glPlatform is EglPlatformOpenGlInterface egl && - egl.Display is AngleWin32EglDisplay angleDisplay && - angleDisplay.PlatformApi == AngleOptions.PlatformApi.DirectX11; + var isUsingAngleDX11 = glPlatform is AngleWin32PlatformGraphics angle && + angle.PlatformApi == AngleOptions.PlatformApi.DirectX11; + _isUsingComposition = compositionConnector is { } && isUsingAngleDX11; DxgiConnection dxgiConnection = null; + var isUsingDxgiSwapchain = false; if (!_isUsingComposition) { dxgiConnection = AvaloniaLocator.Current.GetService(); - _isUsingDxgiSwapchain = dxgiConnection is { } && - glPlatform is EglPlatformOpenGlInterface eglDxgi && - eglDxgi.Display is AngleWin32EglDisplay angleDisplayDxgi && - angleDisplayDxgi.PlatformApi == AngleOptions.PlatformApi.DirectX11; + isUsingDxgiSwapchain = dxgiConnection is { } && isUsingAngleDX11; } _wmPointerEnabled = Win32Platform.WindowsVersion >= PlatformConstants.Windows8; @@ -164,24 +161,24 @@ namespace Avalonia.Win32 { if (_isUsingComposition) { - var cgl = new WinUiCompositedWindowSurface(compositionConnector, this); + var cgl = compositionConnector.CreateSurface(this); _blurHost = cgl; _gl = cgl; _isUsingComposition = true; } - else if (_isUsingDxgiSwapchain) + else if (isUsingDxgiSwapchain) { var dxgigl = new DxgiSwapchainWindow(dxgiConnection, this); _gl = dxgigl; } else { - if (glPlatform is EglPlatformOpenGlInterface egl2) - _gl = new EglGlPlatformSurface(egl2, this); + if (glPlatform is AngleWin32PlatformGraphics egl2) + _gl = new EglGlPlatformSurface(this); else if (glPlatform is WglPlatformOpenGlInterface wgl) - _gl = new WglGlPlatformSurface(wgl.PrimaryContext, this); + _gl = new WglGlPlatformSurface(this); } } @@ -434,7 +431,7 @@ namespace Avalonia.Win32 _ => BlurEffect.None }; - if (Win32Platform.WindowsVersion >= WinUICompositorConnection.MinHostBackdropVersion) + if (Win32Platform.WindowsVersion >= WinUiCompositionShared.MinHostBackdropVersion) { unsafe { @@ -443,7 +440,7 @@ namespace Avalonia.Win32 } } - if (Win32Platform.WindowsVersion < WinUICompositorConnection.MinHostBackdropVersion && effect == BlurEffect.Mica) + if (Win32Platform.WindowsVersion < WinUiCompositionShared.MinHostBackdropVersion && effect == BlurEffect.Mica) { effect = BlurEffect.Acrylic; } @@ -570,16 +567,22 @@ namespace Avalonia.Win32 return customRendererFactory.Create(root, loop); if (Win32Platform.Compositor != null) - return new CompositingRenderer(root, Win32Platform.Compositor); - + return new CompositingRenderer(root, Win32Platform.Compositor, () => Surfaces); + return Win32Platform.UseDeferredRendering ? _isUsingComposition - ? new DeferredRenderer(root, loop) + ? new DeferredRenderer(root, loop, + () => Win32Platform.RenderInterface.CreateRenderTarget(Surfaces), + Win32Platform.RenderInterface) { RenderOnlyOnRenderThread = true } - : (IRenderer)new DeferredRenderer(root, loop, rendererLock: _rendererLock) - : new ImmediateRenderer((Visual)root); + : (IRenderer)new DeferredRenderer(root, loop, rendererLock: _rendererLock, + renderTargetFactory: () => Win32Platform.RenderInterface.CreateRenderTarget(Surfaces), + renderInterface: Win32Platform.RenderInterface) + : new ImmediateRenderer((Visual)root, + () => Win32Platform.RenderInterface.CreateRenderTarget(Surfaces), + Win32Platform.RenderInterface); } public void Resize(Size value, PlatformResizeReason reason) @@ -1340,8 +1343,6 @@ namespace Avalonia.Win32 } private const int MF_BYCOMMAND = 0x0; - private const int MF_BYPOSITION = 0x400; - private const int MF_REMOVE = 0x1000; private const int MF_ENABLED = 0x0; private const int MF_GRAYED = 0x1; private const int MF_DISABLED = 0x2; diff --git a/src/iOS/Avalonia.iOS/AvaloniaView.cs b/src/iOS/Avalonia.iOS/AvaloniaView.cs index 2080352020..48358c745f 100644 --- a/src/iOS/Avalonia.iOS/AvaloniaView.cs +++ b/src/iOS/Avalonia.iOS/AvaloniaView.cs @@ -70,7 +70,8 @@ namespace Avalonia.iOS // No-op } - public IRenderer CreateRenderer(IRenderRoot root) => new CompositingRenderer(root, Platform.Compositor); + public IRenderer CreateRenderer(IRenderRoot root) => + new CompositingRenderer(root, Platform.Compositor, () => Surfaces); public void Invalidate(Rect rect) diff --git a/src/iOS/Avalonia.iOS/EaglDisplay.cs b/src/iOS/Avalonia.iOS/EaglDisplay.cs index 906bbc29e7..7a5e1a496d 100644 --- a/src/iOS/Avalonia.iOS/EaglDisplay.cs +++ b/src/iOS/Avalonia.iOS/EaglDisplay.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Reactive.Disposables; using Avalonia.OpenGL; using Avalonia.Platform; @@ -6,29 +7,37 @@ using OpenGLES; namespace Avalonia.iOS { - class EaglFeature : IPlatformOpenGlInterface + class EaglPlatformGraphics : IPlatformGraphics { - public IGlContext PrimaryContext => Context; - public IGlContext CreateSharedContext() => throw new NotSupportedException(); - IPlatformGpuContext IPlatformGpu.PrimaryContext => PrimaryContext; - public bool CanShareContexts => false; - public bool CanCreateContexts => false; - public IGlContext CreateContext() => throw new System.NotSupportedException(); - public GlContext Context { get; } = new GlContext(); + public IPlatformGraphicsContext GetSharedContext() => Context; + + public bool UsesSharedContext => true; + public IPlatformGraphicsContext CreateContext() => throw new System.NotSupportedException(); + public GlContext Context { get; } + public static GlVersion GlVersion { get; } = new(GlProfileType.OpenGLES, 3, 0); + + public EaglPlatformGraphics() + { + + const string path = "/System/Library/Frameworks/OpenGLES.framework/OpenGLES"; + var libGl = ObjCRuntime.Dlfcn.dlopen(path, 1); + if (libGl == IntPtr.Zero) + throw new OpenGlException("Unable to load " + path); + var iface = new GlInterface(GlVersion, proc => ObjCRuntime.Dlfcn.dlsym(libGl, proc)); + Context = new(iface, null); + } } class GlContext : IGlContext { public EAGLContext Context { get; private set; } - public GlContext() + public GlContext(GlInterface glInterface, EAGLSharegroup sharegroup) { - const string path = "/System/Library/Frameworks/OpenGLES.framework/OpenGLES"; - var libGl = ObjCRuntime.Dlfcn.dlopen(path, 1); - if (libGl == IntPtr.Zero) - throw new OpenGlException("Unable to load " + path); - GlInterface = new GlInterface(Version, proc => ObjCRuntime.Dlfcn.dlsym(libGl, proc)); - Context = new EAGLContext(EAGLRenderingAPI.OpenGLES3); + GlInterface = glInterface; + Context = sharegroup == null ? + new EAGLContext(EAGLRenderingAPI.OpenGLES3) : + new(EAGLRenderingAPI.OpenGLES3, sharegroup); } public void Dispose() @@ -59,22 +68,34 @@ namespace Avalonia.iOS public IDisposable MakeCurrent() { + if (Context == null) + throw new PlatformGraphicsContextLostException(); var old = EAGLContext.CurrentContext; if (!EAGLContext.SetCurrentContext(Context)) throw new OpenGlException("Unable to make context current"); return new ResetContext(old); } + public bool IsLost => Context == null; + public IDisposable EnsureCurrent() { + if (Context == null) + throw new PlatformGraphicsContextLostException(); if(EAGLContext.CurrentContext == Context) return Disposable.Empty; return MakeCurrent(); } - public bool IsSharedWith(IGlContext context) => false; + public bool IsSharedWith(IGlContext context) => context is GlContext other + && ReferenceEquals(other.Context?.ShareGroup, Context?.ShareGroup); + public bool CanCreateSharedContext => true; + public IGlContext CreateSharedContext(IEnumerable preferredVersions = null) + { + return new GlContext(GlInterface, Context.ShareGroup); + } - public GlVersion Version { get; } = new GlVersion(GlProfileType.OpenGLES, 3, 0); + public GlVersion Version => EaglPlatformGraphics.GlVersion; public GlInterface GlInterface { get; } public int SampleCount { @@ -92,5 +113,7 @@ namespace Avalonia.iOS return stencil; } } + + public object TryGetFeature(Type featureType) => null; } } diff --git a/src/iOS/Avalonia.iOS/EaglLayerSurface.cs b/src/iOS/Avalonia.iOS/EaglLayerSurface.cs index 0e8945d921..88812653d7 100644 --- a/src/iOS/Avalonia.iOS/EaglLayerSurface.cs +++ b/src/iOS/Avalonia.iOS/EaglLayerSurface.cs @@ -78,10 +78,12 @@ namespace Avalonia.iOS throw new InvalidOperationException("Invalid thread, go away"); } - public IGlPlatformSurfaceRenderTarget CreateGlRenderTarget() + public IGlPlatformSurfaceRenderTarget CreateGlRenderTarget(IGlContext context) { CheckThread(); var ctx = Platform.GlFeature.Context; + if (ctx != context) + throw new InvalidOperationException("Platform surface is only usable with tha main context"); using (ctx.MakeCurrent()) { var fbo = new SizeSynchronizedLayerFbo(ctx.Context, ctx.GlInterface, _layer); diff --git a/src/iOS/Avalonia.iOS/Platform.cs b/src/iOS/Avalonia.iOS/Platform.cs index c2cf8c6f6c..eb0a55734a 100644 --- a/src/iOS/Avalonia.iOS/Platform.cs +++ b/src/iOS/Avalonia.iOS/Platform.cs @@ -25,18 +25,18 @@ namespace Avalonia.iOS { static class Platform { - public static EaglFeature GlFeature; + public static EaglPlatformGraphics GlFeature; public static DisplayLinkTimer Timer; internal static Compositor Compositor { get; private set; } public static void Register() { - GlFeature ??= new EaglFeature(); + GlFeature ??= new EaglPlatformGraphics(); Timer ??= new DisplayLinkTimer(); var keyboard = new KeyboardDevice(); AvaloniaLocator.CurrentMutable - .Bind().ToConstant(GlFeature) + .Bind().ToConstant(GlFeature) .Bind().ToConstant(new CursorFactoryStub()) .Bind().ToConstant(new WindowingPlatformStub()) .Bind().ToConstant(new ClipboardImpl()) @@ -50,7 +50,7 @@ namespace Avalonia.iOS Compositor = new Compositor( AvaloniaLocator.Current.GetRequiredService(), - AvaloniaLocator.Current.GetService()); + AvaloniaLocator.Current.GetService()); } diff --git a/tests/Avalonia.Base.UnitTests/Animation/AnimationIterationTests.cs b/tests/Avalonia.Base.UnitTests/Animation/AnimationIterationTests.cs index ca24b95e65..58e908aca9 100644 --- a/tests/Avalonia.Base.UnitTests/Animation/AnimationIterationTests.cs +++ b/tests/Avalonia.Base.UnitTests/Animation/AnimationIterationTests.cs @@ -181,7 +181,7 @@ namespace Avalonia.Base.UnitTests.Animation Assert.Equal(border.Width, 300d); } - [Fact(Skip = "See #6111")] + [Fact] public void Dispose_Subscription_Should_Stop_Animation() { var keyframe1 = new KeyFrame() @@ -310,7 +310,7 @@ namespace Avalonia.Base.UnitTests.Animation Assert.True(animationRun.IsCompleted); } - [Fact(Skip = "See #6111")] + [Fact] public void Cancellation_Should_Stop_Animation() { var keyframe1 = new KeyFrame() @@ -372,7 +372,6 @@ namespace Avalonia.Base.UnitTests.Animation clock.Step(TimeSpan.FromSeconds(1)); clock.Step(TimeSpan.FromSeconds(2)); clock.Step(TimeSpan.FromSeconds(3)); - //Assert.Equal(2, propertyChangedCount); animationRun.Wait(); diff --git a/tests/Avalonia.Controls.UnitTests/FlowDirectionTests.cs b/tests/Avalonia.Base.UnitTests/FlowDirectionTests.cs similarity index 70% rename from tests/Avalonia.Controls.UnitTests/FlowDirectionTests.cs rename to tests/Avalonia.Base.UnitTests/FlowDirectionTests.cs index 6c43103ecb..f790ed7412 100644 --- a/tests/Avalonia.Controls.UnitTests/FlowDirectionTests.cs +++ b/tests/Avalonia.Base.UnitTests/FlowDirectionTests.cs @@ -8,7 +8,7 @@ namespace Avalonia.Controls.UnitTests [Fact] public void HasMirrorTransform_Should_Be_True() { - var target = new Control + var target = new Visual { FlowDirection = FlowDirection.RightToLeft, }; @@ -19,31 +19,36 @@ namespace Avalonia.Controls.UnitTests [Fact] public void HasMirrorTransform_Of_LTR_Children_Should_Be_True_For_RTL_Parent() { - Control child; - var target = new Decorator + var child = new Visual() + { + FlowDirection = FlowDirection.LeftToRight, + }; + + var target = new Visual { FlowDirection = FlowDirection.RightToLeft, - Child = child = new Control() }; + target.VisualChildren.Add(child); - child.FlowDirection = FlowDirection.LeftToRight; + child.InvalidateMirrorTransform(); Assert.True(target.HasMirrorTransform); Assert.True(child.HasMirrorTransform); } [Fact] - public void HasMirrorTransform_Of_Children_Is_Updated_After_Parent_Changeed() + public void HasMirrorTransform_Of_Children_Is_Updated_After_Parent_Changed() { - Control child; + var child = new Visual() + { + FlowDirection = FlowDirection.LeftToRight, + }; + var target = new Decorator { FlowDirection = FlowDirection.LeftToRight, - Child = child = new Control() - { - FlowDirection = FlowDirection.LeftToRight, - } }; + target.VisualChildren.Add(child); Assert.False(target.HasMirrorTransform); Assert.False(child.HasMirrorTransform); diff --git a/tests/Avalonia.Base.UnitTests/Input/InputElement_Focus.cs b/tests/Avalonia.Base.UnitTests/Input/InputElement_Focus.cs index 2d8ee62ef2..e36ce21009 100644 --- a/tests/Avalonia.Base.UnitTests/Input/InputElement_Focus.cs +++ b/tests/Avalonia.Base.UnitTests/Input/InputElement_Focus.cs @@ -1,5 +1,7 @@ +using System; using Avalonia.Controls; using Avalonia.Input; +using Avalonia.Interactivity; using Avalonia.UnitTests; using Xunit; @@ -25,6 +27,33 @@ namespace Avalonia.Base.UnitTests.Input } } + [Fact] + public void Focus_Should_Not_Get_Restored_To_Enabled_Control() + { + using (UnitTestApplication.Start(TestServices.RealFocus)) + { + var sp = new StackPanel(); + Button target = new Button(); + Button target1 = new Button(); + target.Click += (s, e) => target.IsEnabled = false; + target1.Click += (s, e) => target.IsEnabled = true; + sp.Children.Add(target); + sp.Children.Add(target1); + var root = new TestRoot + { + Child = sp + }; + + target.Focus(); + target.RaiseEvent(new RoutedEventArgs(AccessKeyHandler.AccessKeyPressedEvent)); + Assert.False(target.IsEnabled); + Assert.False(target.IsFocused); + target1.RaiseEvent(new RoutedEventArgs(AccessKeyHandler.AccessKeyPressedEvent)); + Assert.True(target.IsEnabled); + Assert.False(target.IsFocused); + } + } + [Fact] public void Focus_Should_Be_Cleared_When_Control_Is_Removed_From_VisualTree() { diff --git a/tests/Avalonia.Base.UnitTests/Media/GlyphRunTests.cs b/tests/Avalonia.Base.UnitTests/Media/GlyphRunTests.cs index df16c1b34f..363fb3f5b3 100644 --- a/tests/Avalonia.Base.UnitTests/Media/GlyphRunTests.cs +++ b/tests/Avalonia.Base.UnitTests/Media/GlyphRunTests.cs @@ -181,9 +181,7 @@ namespace Avalonia.Base.UnitTests.Media var count = glyphAdvances.Length; var glyphIndices = new ushort[count]; - var start = bidiLevel == 0 ? glyphClusters[0] : glyphClusters[^1]; - - var characters = new ReadOnlySlice(Enumerable.Repeat('a', count).ToArray(), start, count); + var characters = Enumerable.Repeat('a', count).ToArray(); return new GlyphRun(new MockGlyphTypeface(), 10, characters, glyphIndices, glyphAdvances, glyphClusters: glyphClusters, biDiLevel: bidiLevel); diff --git a/tests/Avalonia.Base.UnitTests/Media/TextFormatting/BiDiClassTests.cs b/tests/Avalonia.Base.UnitTests/Media/TextFormatting/BiDiClassTests.cs index d2cea45ce1..b2c40f4ff1 100644 --- a/tests/Avalonia.Base.UnitTests/Media/TextFormatting/BiDiClassTests.cs +++ b/tests/Avalonia.Base.UnitTests/Media/TextFormatting/BiDiClassTests.cs @@ -2,6 +2,7 @@ using System.Linq; using System.Runtime.InteropServices; using System.Text; +using Avalonia.Media.TextFormatting; using Avalonia.Media.TextFormatting.Unicode; using Xunit; using Xunit.Abstractions; @@ -36,7 +37,7 @@ namespace Avalonia.Visuals.UnitTests.Media.TextFormatting var text = Encoding.UTF32.GetString(MemoryMarshal.Cast(t.CodePoints).ToArray()); // Append - bidiData.Append(text.AsMemory()); + bidiData.Append(new CharacterBufferRange(text)); // Act for (int i = 0; i < 10; i++) diff --git a/tests/Avalonia.Base.UnitTests/Media/TextFormatting/GraphemeBreakClassTrieGeneratorTests.cs b/tests/Avalonia.Base.UnitTests/Media/TextFormatting/GraphemeBreakClassTrieGeneratorTests.cs index c9f869cea9..4e0207a85d 100644 --- a/tests/Avalonia.Base.UnitTests/Media/TextFormatting/GraphemeBreakClassTrieGeneratorTests.cs +++ b/tests/Avalonia.Base.UnitTests/Media/TextFormatting/GraphemeBreakClassTrieGeneratorTests.cs @@ -1,6 +1,7 @@ using System; using System.Runtime.InteropServices; using System.Text; +using Avalonia.Media.TextFormatting; using Avalonia.Media.TextFormatting.Unicode; using Avalonia.Visuals.UnitTests.Media.TextFormatting; using Xunit; @@ -37,11 +38,11 @@ namespace Avalonia.Base.UnitTests.Media.TextFormatting var text = Encoding.UTF32.GetString(MemoryMarshal.Cast(t.Codepoints).ToArray()); var grapheme = Encoding.UTF32.GetString(MemoryMarshal.Cast(t.Grapheme).ToArray()).AsSpan(); - var enumerator = new GraphemeEnumerator(text.AsMemory()); + var enumerator = new GraphemeEnumerator(new CharacterBufferRange(text)); enumerator.MoveNext(); - var actual = enumerator.Current.Text.Span; + var actual = enumerator.Current.Text; var pass = true; @@ -86,9 +87,7 @@ namespace Avalonia.Base.UnitTests.Media.TextFormatting { const string text = "ABCDEFGHIJ"; - var textMemory = text.AsMemory(); - - var enumerator = new GraphemeEnumerator(textMemory); + var enumerator = new GraphemeEnumerator(new CharacterBufferRange(text)); var count = 0; diff --git a/tests/Avalonia.Base.UnitTests/Media/TextFormatting/LineBreakEnumuratorTests.cs b/tests/Avalonia.Base.UnitTests/Media/TextFormatting/LineBreakEnumuratorTests.cs index 15be1200c8..b2648bf348 100644 --- a/tests/Avalonia.Base.UnitTests/Media/TextFormatting/LineBreakEnumuratorTests.cs +++ b/tests/Avalonia.Base.UnitTests/Media/TextFormatting/LineBreakEnumuratorTests.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; using System.Net.Http; +using Avalonia.Media.TextFormatting; using Avalonia.Media.TextFormatting.Unicode; using Xunit; using Xunit.Abstractions; @@ -22,7 +23,7 @@ namespace Avalonia.Base.UnitTests.Media.TextFormatting [Fact] public void BasicLatinTest() { - var lineBreaker = new LineBreakEnumerator("Hello World\r\nThis is a test.".AsMemory()); + var lineBreaker = new LineBreakEnumerator(new CharacterBufferRange("Hello World\r\nThis is a test.")); Assert.True(lineBreaker.MoveNext()); Assert.Equal(6, lineBreaker.Current.PositionWrap); @@ -55,7 +56,7 @@ namespace Avalonia.Base.UnitTests.Media.TextFormatting [Fact] public void ForwardTextWithOuterWhitespace() { - var lineBreaker = new LineBreakEnumerator(" Apples Pears Bananas ".AsMemory()); + var lineBreaker = new LineBreakEnumerator(new CharacterBufferRange(" Apples Pears Bananas ")); var positionsF = GetBreaks(lineBreaker); Assert.Equal(1, positionsF[0].PositionWrap); Assert.Equal(0, positionsF[0].PositionMeasure); @@ -82,7 +83,7 @@ namespace Avalonia.Base.UnitTests.Media.TextFormatting [Fact] public void ForwardTest() { - var lineBreaker = new LineBreakEnumerator("Apples Pears Bananas".AsMemory()); + var lineBreaker = new LineBreakEnumerator(new CharacterBufferRange("Apples Pears Bananas")); var positionsF = GetBreaks(lineBreaker); Assert.Equal(7, positionsF[0].PositionWrap); @@ -99,7 +100,7 @@ namespace Avalonia.Base.UnitTests.Media.TextFormatting { var text = string.Join(null, codePoints.Select(char.ConvertFromUtf32)); - var lineBreaker = new LineBreakEnumerator(text.AsMemory()); + var lineBreaker = new LineBreakEnumerator(new CharacterBufferRange(text)); var foundBreaks = new List(); diff --git a/tests/Avalonia.Base.UnitTests/RectTests.cs b/tests/Avalonia.Base.UnitTests/RectTests.cs index 95a438b287..c44b328ed5 100644 --- a/tests/Avalonia.Base.UnitTests/RectTests.cs +++ b/tests/Avalonia.Base.UnitTests/RectTests.cs @@ -52,7 +52,7 @@ namespace Avalonia.Base.UnitTests double.PositiveInfinity, double.PositiveInfinity) .Normalize(); - Assert.Equal(Rect.Empty, result); + Assert.Equal(default, result); } } } diff --git a/tests/Avalonia.Base.UnitTests/Rendering/CompositorTestsBase.cs b/tests/Avalonia.Base.UnitTests/Rendering/CompositorTestsBase.cs index 7b987d8e68..d407a09b06 100644 --- a/tests/Avalonia.Base.UnitTests/Rendering/CompositorTestsBase.cs +++ b/tests/Avalonia.Base.UnitTests/Rendering/CompositorTestsBase.cs @@ -76,7 +76,7 @@ public class CompositorTestsBase public IRenderer CreateRenderer(IRenderRoot root) { - return Renderer = new CompositingRenderer(root, _compositor); + return Renderer = new CompositingRenderer(root, _compositor, () => Surfaces); } public void Invalidate(Rect rect) diff --git a/tests/Avalonia.Base.UnitTests/Rendering/DeferredRendererTests.cs b/tests/Avalonia.Base.UnitTests/Rendering/DeferredRendererTests.cs index 08e5955ec5..f0c5a24cc4 100644 --- a/tests/Avalonia.Base.UnitTests/Rendering/DeferredRendererTests.cs +++ b/tests/Avalonia.Base.UnitTests/Rendering/DeferredRendererTests.cs @@ -46,6 +46,7 @@ namespace Avalonia.Base.UnitTests.Rendering var target = new DeferredRenderer( root, loop.Object, + renderTargetFactory: root.CreateRenderTarget, sceneBuilder: sceneBuilder.Object); target.Start(); @@ -83,6 +84,7 @@ namespace Avalonia.Base.UnitTests.Rendering root, loop.Object, sceneBuilder: sceneBuilder.Object, + renderTargetFactory: root.CreateRenderTarget, dispatcher: dispatcher); target.Start(); @@ -133,6 +135,7 @@ namespace Avalonia.Base.UnitTests.Rendering root, loop.Object, sceneBuilder: sceneBuilder, + renderTargetFactory: root.CreateRenderTarget, dispatcher: dispatcher); root.Renderer = target; @@ -178,6 +181,7 @@ namespace Avalonia.Base.UnitTests.Rendering root, loop.Object, sceneBuilder: sceneBuilder, + renderTargetFactory: root.CreateRenderTarget, dispatcher: dispatcher); root.Renderer = target; @@ -223,6 +227,7 @@ namespace Avalonia.Base.UnitTests.Rendering root, loop.Object, sceneBuilder: sceneBuilder, + renderTargetFactory: root.CreateRenderTarget, dispatcher: dispatcher); root.Renderer = target; @@ -270,6 +275,7 @@ namespace Avalonia.Base.UnitTests.Rendering root, loop.Object, sceneBuilder: sceneBuilder, + renderTargetFactory: root.CreateRenderTarget, dispatcher: dispatcher); root.Renderer = target; @@ -317,6 +323,7 @@ namespace Avalonia.Base.UnitTests.Rendering root, loop.Object, sceneBuilder: sceneBuilder, + renderTargetFactory: root.CreateRenderTarget, dispatcher: dispatcher); root.Renderer = target; @@ -366,6 +373,7 @@ namespace Avalonia.Base.UnitTests.Rendering root, loop.Object, sceneBuilder: sceneBuilder, + renderTargetFactory: root.CreateRenderTarget, dispatcher: dispatcher); root.Renderer = target; @@ -415,6 +423,7 @@ namespace Avalonia.Base.UnitTests.Rendering root, loop.Object, sceneBuilder: sceneBuilder, + renderTargetFactory: root.CreateRenderTarget, dispatcher: dispatcher); var otherSceneBuilder = new SceneBuilder(); @@ -423,6 +432,7 @@ namespace Avalonia.Base.UnitTests.Rendering otherRoot, loop.Object, sceneBuilder: otherSceneBuilder, + renderTargetFactory: root.CreateRenderTarget, dispatcher: dispatcher); root.Renderer = target; @@ -738,6 +748,7 @@ namespace Avalonia.Base.UnitTests.Rendering root, new RenderLoop(timer.Object, dispatcher), sceneBuilder: sceneBuilder, + renderTargetFactory: root.CreateRenderTarget, dispatcher: dispatcher); root.Renderer = target; target.Start(); diff --git a/tests/Avalonia.Base.UnitTests/Rendering/DeferredRendererTests_HitTesting.cs b/tests/Avalonia.Base.UnitTests/Rendering/DeferredRendererTests_HitTesting.cs index 2cf42d9604..4f11af7327 100644 --- a/tests/Avalonia.Base.UnitTests/Rendering/DeferredRendererTests_HitTesting.cs +++ b/tests/Avalonia.Base.UnitTests/Rendering/DeferredRendererTests_HitTesting.cs @@ -35,7 +35,7 @@ namespace Avalonia.Base.UnitTests.Rendering } }; - root.Renderer = new DeferredRenderer((IRenderRoot)root, null); + root.Renderer = new DeferredRenderer((IRenderRoot)root, null, root.CreateRenderTarget); root.Measure(Size.Infinity); root.Arrange(new Rect(root.DesiredSize)); @@ -63,7 +63,7 @@ namespace Avalonia.Base.UnitTests.Rendering } }; - root.Renderer = new DeferredRenderer((IRenderRoot)root, null); + root.Renderer = new DeferredRenderer((IRenderRoot)root, null, root.CreateRenderTarget); root.Measure(Size.Infinity); root.Arrange(new Rect(root.DesiredSize)); @@ -100,7 +100,7 @@ namespace Avalonia.Base.UnitTests.Rendering } }; - root.Renderer = new DeferredRenderer((IRenderRoot)root, null); + root.Renderer = new DeferredRenderer((IRenderRoot)root, null, root.CreateRenderTarget); root.Measure(Size.Infinity); root.Arrange(new Rect(root.DesiredSize)); @@ -129,7 +129,7 @@ namespace Avalonia.Base.UnitTests.Rendering } }; - root.Renderer = new DeferredRenderer((IRenderRoot)root, null); + root.Renderer = new DeferredRenderer((IRenderRoot)root, null, root.CreateRenderTarget); root.Measure(Size.Infinity); root.Arrange(new Rect(root.DesiredSize)); @@ -173,7 +173,7 @@ namespace Avalonia.Base.UnitTests.Rendering } }; - root.Renderer = new DeferredRenderer((IRenderRoot)root, null); + root.Renderer = new DeferredRenderer((IRenderRoot)root, null, root.CreateRenderTarget); root.Measure(Size.Infinity); root.Arrange(new Rect(container.DesiredSize)); @@ -227,7 +227,7 @@ namespace Avalonia.Base.UnitTests.Rendering } }; - root.Renderer = new DeferredRenderer((IRenderRoot)root, null); + root.Renderer = new DeferredRenderer((IRenderRoot)root, null, root.CreateRenderTarget); root.Measure(Size.Infinity); root.Arrange(new Rect(container.DesiredSize)); @@ -276,7 +276,7 @@ namespace Avalonia.Base.UnitTests.Rendering } }; - root.Renderer = new DeferredRenderer((IRenderRoot)root, null); + root.Renderer = new DeferredRenderer((IRenderRoot)root, null, root.CreateRenderTarget); container.Measure(Size.Infinity); container.Arrange(new Rect(container.DesiredSize)); @@ -324,7 +324,7 @@ namespace Avalonia.Base.UnitTests.Rendering } }; - root.Renderer = new DeferredRenderer((IRenderRoot)root, null); + root.Renderer = new DeferredRenderer((IRenderRoot)root, null, root.CreateRenderTarget); root.Measure(Size.Infinity); root.Arrange(new Rect(container.DesiredSize)); @@ -399,11 +399,11 @@ namespace Avalonia.Base.UnitTests.Rendering scroll.UpdateChild(); - root.Renderer = new DeferredRenderer((IRenderRoot)root, null); + root.Renderer = new DeferredRenderer((IRenderRoot)root, null, root.CreateRenderTarget); root.Measure(Size.Infinity); root.Arrange(new Rect(container.DesiredSize)); - root.Renderer.Paint(Rect.Empty); + root.Renderer.Paint(default); var result = root.Renderer.HitTest(new Point(50, 150), root, null).First(); Assert.Equal(item1, result); @@ -419,7 +419,7 @@ namespace Avalonia.Base.UnitTests.Rendering container.InvalidateArrange(); container.Arrange(new Rect(container.DesiredSize)); - root.Renderer.Paint(Rect.Empty); + root.Renderer.Paint(default); result = root.Renderer.HitTest(new Point(50, 150), root, null).First(); Assert.Equal(item2, result); @@ -447,7 +447,7 @@ namespace Avalonia.Base.UnitTests.Rendering } }; - root.Renderer = new DeferredRenderer((IRenderRoot)root, null); + root.Renderer = new DeferredRenderer((IRenderRoot)root, null, root.CreateRenderTarget); root.Measure(Size.Infinity); root.Arrange(new Rect(root.DesiredSize)); @@ -486,7 +486,7 @@ namespace Avalonia.Base.UnitTests.Rendering } }; - root.Renderer = new DeferredRenderer((IRenderRoot)root, null); + root.Renderer = new DeferredRenderer((IRenderRoot)root, null, root.CreateRenderTarget); root.Measure(Size.Infinity); root.Arrange(new Rect(root.DesiredSize)); Assert.Equal(new Rect(100, 100, 200, 200), border.Bounds); @@ -522,7 +522,7 @@ namespace Avalonia.Base.UnitTests.Rendering } }; - root.Renderer = new DeferredRenderer((IRenderRoot)root, null); + root.Renderer = new DeferredRenderer((IRenderRoot)root, null, root.CreateRenderTarget); root.Measure(Size.Infinity); root.Arrange(new Rect(root.DesiredSize)); @@ -560,7 +560,7 @@ namespace Avalonia.Base.UnitTests.Rendering } }; - root.Renderer = new DeferredRenderer((IRenderRoot)root, null); + root.Renderer = new DeferredRenderer((IRenderRoot)root, null, root.CreateRenderTarget); root.Measure(Size.Infinity); root.Arrange(new Rect(root.DesiredSize)); diff --git a/tests/Avalonia.Base.UnitTests/Rendering/ImmediateRendererTests.cs b/tests/Avalonia.Base.UnitTests/Rendering/ImmediateRendererTests.cs index 26a155d203..a07191f464 100644 --- a/tests/Avalonia.Base.UnitTests/Rendering/ImmediateRendererTests.cs +++ b/tests/Avalonia.Base.UnitTests/Rendering/ImmediateRendererTests.cs @@ -36,7 +36,7 @@ namespace Avalonia.Base.UnitTests.Rendering root.LayoutManager.ExecuteInitialLayoutPass(); - var target = new ImmediateRenderer(root); + var target = new ImmediateRenderer(root, root.CreateRenderTarget); target.AddDirty(child); @@ -69,7 +69,7 @@ namespace Avalonia.Base.UnitTests.Rendering child.RenderTransform = new ScaleTransform() { ScaleX = 2, ScaleY = 2 }; - var target = new ImmediateRenderer(root); + var target = new ImmediateRenderer(root, root.CreateRenderTarget); target.AddDirty(child); @@ -97,7 +97,7 @@ namespace Avalonia.Base.UnitTests.Rendering Height = 400, }; - var target = new ImmediateRenderer(root); + var target = new ImmediateRenderer(root, root.CreateRenderTarget); root.LayoutManager.ExecuteInitialLayoutPass(); target.AddDirty(child); @@ -165,7 +165,7 @@ namespace Avalonia.Base.UnitTests.Rendering stackPanel.Children.Add(control3); var root = new TestRoot(rootGrid); - root.Renderer = new ImmediateRenderer(root); + root.Renderer = new ImmediateRenderer(root, root.CreateRenderTarget); root.LayoutManager.ExecuteInitialLayoutPass(); var rootSize = new Size(RootWidth, RootHeight); @@ -223,7 +223,7 @@ namespace Avalonia.Base.UnitTests.Rendering stackPanel.Children.Add(control3); var root = new TestRoot(rootGrid); - root.Renderer = new ImmediateRenderer(root); + root.Renderer = new ImmediateRenderer(root, root.CreateRenderTarget); root.LayoutManager.ExecuteInitialLayoutPass(); var rootSize = new Size(RootWidth, RootHeight); diff --git a/tests/Avalonia.Base.UnitTests/Rendering/ImmediateRendererTests_HitTesting.cs b/tests/Avalonia.Base.UnitTests/Rendering/ImmediateRendererTests_HitTesting.cs index 70e4acba1e..9ce8c42e33 100644 --- a/tests/Avalonia.Base.UnitTests/Rendering/ImmediateRendererTests_HitTesting.cs +++ b/tests/Avalonia.Base.UnitTests/Rendering/ImmediateRendererTests_HitTesting.cs @@ -32,7 +32,7 @@ namespace Avalonia.Base.UnitTests.Rendering } }; - root.Renderer = new ImmediateRenderer(root); + root.Renderer = new ImmediateRenderer(root, root.CreateRenderTarget); root.Measure(Size.Infinity); root.Arrange(new Rect(root.DesiredSize)); root.Renderer.Paint(new Rect(root.ClientSize)); @@ -70,7 +70,7 @@ namespace Avalonia.Base.UnitTests.Rendering } }; - root.Renderer = new ImmediateRenderer(root); + root.Renderer = new ImmediateRenderer(root, root.CreateRenderTarget); root.Measure(Size.Infinity); root.Arrange(new Rect(root.DesiredSize)); root.Renderer.Paint(new Rect(root.ClientSize)); @@ -100,7 +100,7 @@ namespace Avalonia.Base.UnitTests.Rendering } }; - root.Renderer = new ImmediateRenderer(root); + root.Renderer = new ImmediateRenderer(root, root.CreateRenderTarget); root.Measure(Size.Infinity); root.Arrange(new Rect(root.DesiredSize)); root.Renderer.Paint(new Rect(root.ClientSize)); @@ -145,7 +145,7 @@ namespace Avalonia.Base.UnitTests.Rendering } }; - root.Renderer = new ImmediateRenderer(root); + root.Renderer = new ImmediateRenderer(root, root.CreateRenderTarget); root.Measure(Size.Infinity); root.Arrange(new Rect(container.DesiredSize)); root.Renderer.Paint(new Rect(root.ClientSize)); @@ -200,7 +200,7 @@ namespace Avalonia.Base.UnitTests.Rendering } }; - root.Renderer = new ImmediateRenderer(root); + root.Renderer = new ImmediateRenderer(root, root.CreateRenderTarget); root.Measure(Size.Infinity); root.Arrange(new Rect(container.DesiredSize)); root.Renderer.Paint(new Rect(root.ClientSize)); @@ -259,7 +259,7 @@ namespace Avalonia.Base.UnitTests.Rendering } }; - root.Renderer = new ImmediateRenderer(root); + root.Renderer = new ImmediateRenderer(root, root.CreateRenderTarget); container.Measure(Size.Infinity); container.Arrange(new Rect(container.DesiredSize)); root.Renderer.Paint(new Rect(root.ClientSize)); @@ -308,7 +308,7 @@ namespace Avalonia.Base.UnitTests.Rendering } }; - root.Renderer = new ImmediateRenderer(root); + root.Renderer = new ImmediateRenderer(root, root.CreateRenderTarget); root.Measure(Size.Infinity); root.Arrange(new Rect(container.DesiredSize)); root.Renderer.Paint(new Rect(root.ClientSize)); @@ -384,7 +384,7 @@ namespace Avalonia.Base.UnitTests.Rendering scroll.UpdateChild(); - root.Renderer = new ImmediateRenderer(root); + root.Renderer = new ImmediateRenderer(root, root.CreateRenderTarget); root.Measure(Size.Infinity); root.Arrange(new Rect(container.DesiredSize)); root.Renderer.Paint(new Rect(root.ClientSize)); @@ -434,7 +434,7 @@ namespace Avalonia.Base.UnitTests.Rendering } }; - root.Renderer = new ImmediateRenderer(root); + root.Renderer = new ImmediateRenderer(root, root.CreateRenderTarget); root.Measure(Size.Infinity); root.Arrange(new Rect(root.DesiredSize)); root.Renderer.Paint(new Rect(root.ClientSize)); diff --git a/tests/Avalonia.Base.UnitTests/Rendering/SceneGraph/DrawOperationTests.cs b/tests/Avalonia.Base.UnitTests/Rendering/SceneGraph/DrawOperationTests.cs index 2fac968206..07d2d672ae 100644 --- a/tests/Avalonia.Base.UnitTests/Rendering/SceneGraph/DrawOperationTests.cs +++ b/tests/Avalonia.Base.UnitTests/Rendering/SceneGraph/DrawOperationTests.cs @@ -13,9 +13,9 @@ namespace Avalonia.Base.UnitTests.Rendering.SceneGraph [Fact] public void Empty_Bounds_Remain_Empty() { - var target = new TestDrawOperation(Rect.Empty, Matrix.Identity, null); + var target = new TestDrawOperation(default, Matrix.Identity, null); - Assert.Equal(Rect.Empty, target.Bounds); + Assert.Equal(default, target.Bounds); } [Theory] diff --git a/tests/Avalonia.Base.UnitTests/Utilities/ReadOnlySpanTests.cs b/tests/Avalonia.Base.UnitTests/Utilities/ReadOnlySpanTests.cs deleted file mode 100644 index da30ee9d02..0000000000 --- a/tests/Avalonia.Base.UnitTests/Utilities/ReadOnlySpanTests.cs +++ /dev/null @@ -1,37 +0,0 @@ -using System.Linq; -using Avalonia.Utilities; -using Xunit; - -namespace Avalonia.Base.UnitTests.Utilities -{ - public class ReadOnlySpanTests - { - [Fact] - public void Should_Skip() - { - var buffer = new[] { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 }; - - var slice = new ReadOnlySlice(buffer); - - var skipped = slice.Skip(2); - - var expected = buffer.Skip(2); - - Assert.Equal(expected, skipped); - } - - [Fact] - public void Should_Take() - { - var buffer = new[] { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 }; - - var slice = new ReadOnlySlice(buffer); - - var taken = slice.Take(8); - - var expected = buffer.Take(8); - - Assert.Equal(expected, taken); - } - } -} diff --git a/tests/Avalonia.Base.UnitTests/VisualTree/MockRenderInterface.cs b/tests/Avalonia.Base.UnitTests/VisualTree/MockRenderInterface.cs index 10db08f302..42e33729ac 100644 --- a/tests/Avalonia.Base.UnitTests/VisualTree/MockRenderInterface.cs +++ b/tests/Avalonia.Base.UnitTests/VisualTree/MockRenderInterface.cs @@ -8,13 +8,15 @@ using Avalonia.Media.Imaging; namespace Avalonia.Base.UnitTests.VisualTree { - class MockRenderInterface : IPlatformRenderInterface + class MockRenderInterface : IPlatformRenderInterface, IPlatformRenderInterfaceContext { public IRenderTarget CreateRenderTarget(IEnumerable surfaces) { throw new NotImplementedException(); } + public object TryGetFeature(Type featureType) => null; + public IRenderTargetBitmapImpl CreateRenderTargetBitmap(PixelSize size, Vector dpi) { throw new NotImplementedException(); @@ -77,6 +79,11 @@ namespace Avalonia.Base.UnitTests.VisualTree throw new NotImplementedException(); } + public IPlatformRenderInterfaceContext CreateBackendContext(IPlatformGraphicsContext graphicsContext) + { + return this; + } + public bool SupportsIndividualRoundRects { get; set; } public AlphaFormat DefaultAlphaFormat { get; } public PixelFormat DefaultPixelFormat { get; } @@ -263,6 +270,11 @@ namespace Avalonia.Base.UnitTests.VisualTree } } } + + public void Dispose() + { + + } } } diff --git a/tests/Avalonia.Base.UnitTests/VisualTree/VisualExtensions_GetVisualsAt.cs b/tests/Avalonia.Base.UnitTests/VisualTree/VisualExtensions_GetVisualsAt.cs index b40ea40985..0c516a0481 100644 --- a/tests/Avalonia.Base.UnitTests/VisualTree/VisualExtensions_GetVisualsAt.cs +++ b/tests/Avalonia.Base.UnitTests/VisualTree/VisualExtensions_GetVisualsAt.cs @@ -44,7 +44,7 @@ namespace Avalonia.Base.UnitTests.VisualTree } }; - root.Renderer = new DeferredRenderer((IRenderRoot)root, null); + root.Renderer = new DeferredRenderer((IRenderRoot)root, null, root.CreateRenderTarget); root.Measure(Size.Infinity); root.Arrange(new Rect(root.DesiredSize)); @@ -86,7 +86,7 @@ namespace Avalonia.Base.UnitTests.VisualTree } }; - root.Renderer = new DeferredRenderer((IRenderRoot)root, null); + root.Renderer = new DeferredRenderer((IRenderRoot)root, null, root.CreateRenderTarget); root.Measure(Size.Infinity); root.Arrange(new Rect(root.DesiredSize)); diff --git a/tests/Avalonia.Benchmarks/NullRenderer.cs b/tests/Avalonia.Benchmarks/NullRenderer.cs index 16f8998038..feb325f630 100644 --- a/tests/Avalonia.Benchmarks/NullRenderer.cs +++ b/tests/Avalonia.Benchmarks/NullRenderer.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Threading.Tasks; using Avalonia.Rendering; using Avalonia.VisualTree; @@ -43,5 +44,7 @@ namespace Avalonia.Benchmarks public void Stop() { } + + public ValueTask TryGetRenderInterfaceFeature(Type featureType) => new(0); } } diff --git a/tests/Avalonia.Benchmarks/NullRenderingPlatform.cs b/tests/Avalonia.Benchmarks/NullRenderingPlatform.cs index 4170de71e6..a272d89b8a 100644 --- a/tests/Avalonia.Benchmarks/NullRenderingPlatform.cs +++ b/tests/Avalonia.Benchmarks/NullRenderingPlatform.cs @@ -9,7 +9,7 @@ using Microsoft.Diagnostics.Runtime; namespace Avalonia.Benchmarks { - internal class NullRenderingPlatform : IPlatformRenderInterface + internal class NullRenderingPlatform : IPlatformRenderInterface, IPlatformRenderInterfaceContext { public IGeometryImpl CreateEllipseGeometry(Rect rect) { @@ -46,6 +46,8 @@ namespace Avalonia.Benchmarks throw new NotImplementedException(); } + public object TryGetFeature(Type featureType) => null; + public IRenderTargetBitmapImpl CreateRenderTargetBitmap(PixelSize size, Vector dpi) { throw new NotImplementedException(); @@ -123,10 +125,19 @@ namespace Avalonia.Benchmarks return new MockGlyphRun(); } + public IPlatformRenderInterfaceContext CreateBackendContext(IPlatformGraphicsContext graphicsContext) + { + return this; + } + public bool SupportsIndividualRoundRects => true; public AlphaFormat DefaultAlphaFormat => AlphaFormat.Premul; public PixelFormat DefaultPixelFormat => PixelFormat.Rgba8888; + public void Dispose() + { + + } } } diff --git a/tests/Avalonia.Benchmarks/Themes/ThemeBenchmark.cs b/tests/Avalonia.Benchmarks/Themes/ThemeBenchmark.cs index e82576c7d9..70636d1fe6 100644 --- a/tests/Avalonia.Benchmarks/Themes/ThemeBenchmark.cs +++ b/tests/Avalonia.Benchmarks/Themes/ThemeBenchmark.cs @@ -16,6 +16,8 @@ namespace Avalonia.Benchmarks.Themes public class ThemeBenchmark : IDisposable { private IDisposable _app; + private readonly FluentTheme _reusableFluentTheme = new FluentTheme(); + private readonly SimpleTheme _reusableSimpleTheme = new SimpleTheme(); public ThemeBenchmark() { @@ -49,6 +51,26 @@ namespace Avalonia.Benchmarks.Themes }; return ((IResourceHost)UnitTestApplication.Current).TryGetResource("ThemeAccentColor", out _); } + + [Benchmark] + [Arguments(typeof(Button))] + [Arguments(typeof(TextBox))] + [Arguments(typeof(DatePicker))] + public object FindFluentControlTheme(Type type) + { + _reusableFluentTheme.TryGetResource(type, out var theme); + return theme; + } + + [Benchmark] + [Arguments(typeof(Button))] + [Arguments(typeof(TextBox))] + [Arguments(typeof(DatePicker))] + public object FindSimpleControlTheme(Type type) + { + _reusableSimpleTheme.TryGetResource(type, out var theme); + return theme; + } public void Dispose() { diff --git a/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization_Simple.cs b/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization_Simple.cs index 08fd777ac6..2d1dad6be0 100644 --- a/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization_Simple.cs +++ b/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization_Simple.cs @@ -569,8 +569,8 @@ namespace Avalonia.Controls.UnitTests.Presenters var target = CreateTarget(itemCount: 10); var items = (IList)target.Items; target.ApplyTemplate(); - target.Measure(Size.Empty); - target.Arrange(Rect.Empty); + target.Measure(default(Size)); + target.Arrange(default); // Check for issue #591: this should not throw. target.ScrollIntoView(0); diff --git a/tests/Avalonia.Controls.UnitTests/Presenters/TextPresenter_Tests.cs b/tests/Avalonia.Controls.UnitTests/Presenters/TextPresenter_Tests.cs index 8cc8e4c16f..8e06fbd831 100644 --- a/tests/Avalonia.Controls.UnitTests/Presenters/TextPresenter_Tests.cs +++ b/tests/Avalonia.Controls.UnitTests/Presenters/TextPresenter_Tests.cs @@ -46,7 +46,7 @@ namespace Avalonia.Controls.UnitTests.Presenters Assert.NotNull(target.TextLayout); var actual = string.Join(null, - target.TextLayout.TextLines.SelectMany(x => x.TextRuns).Select(x => x.Text.Span.ToString())); + target.TextLayout.TextLines.SelectMany(x => x.TextRuns).Select(x => x.CharacterBufferReference.CharacterBuffer.Span.ToString())); Assert.Equal("****", actual); } diff --git a/tests/Avalonia.Controls.UnitTests/Selection/SelectionModelTests_Single.cs b/tests/Avalonia.Controls.UnitTests/Selection/SelectionModelTests_Single.cs index 668af3b5d7..1b629be695 100644 --- a/tests/Avalonia.Controls.UnitTests/Selection/SelectionModelTests_Single.cs +++ b/tests/Avalonia.Controls.UnitTests/Selection/SelectionModelTests_Single.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Specialized; +using System.Linq; using Avalonia.Collections; using Avalonia.Controls.Selection; using Avalonia.Controls.Utils; @@ -1144,6 +1145,24 @@ namespace Avalonia.Controls.UnitTests.Selection Assert.Equal(new[] { "foo" }, target.SelectedItems); Assert.Equal(0, target.AnchorIndex); } + + [Fact] + public void SelectedItems_Indexer_Is_Correct() + { + // Issue #7974 + var target = CreateTarget(); + var raised = 0; + + target.SelectionChanged += (s, e) => + { + Assert.Equal("bar", e.SelectedItems.First()); + Assert.Equal("bar", e.SelectedItems[0]); + ++raised; + }; + + target.Select(1); + Assert.Equal(1, raised); + } } public class BatchUpdate diff --git a/tests/Avalonia.Controls.UnitTests/Shapes/RectangleTests.cs b/tests/Avalonia.Controls.UnitTests/Shapes/RectangleTests.cs index 8d8ce10d4c..067d709ae1 100644 --- a/tests/Avalonia.Controls.UnitTests/Shapes/RectangleTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Shapes/RectangleTests.cs @@ -18,7 +18,7 @@ namespace Avalonia.Controls.UnitTests.Shapes target.Measure(new Size(100, 100)); var geometry = Assert.IsType(target.RenderedGeometry); - Assert.Equal(Rect.Empty, geometry.Rect); + Assert.Equal(default, geometry.Rect); } [Fact] diff --git a/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs b/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs index 6b9105ccb5..bd6d5d55e2 100644 --- a/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs +++ b/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs @@ -59,6 +59,28 @@ namespace Avalonia.Controls.UnitTests } } + [Fact] + public void TextBox_Should_Lose_Focus_When_Disabled() + { + using (UnitTestApplication.Start(FocusServices)) + { + var target = new TextBox + { + Template = CreateTemplate() + }; + + target.ApplyTemplate(); + + var root = new TestRoot() { Child = target }; + + target.Focus(); + Assert.True(target.IsFocused); + target.IsEnabled = false; + Assert.False(target.IsFocused); + Assert.False(target.IsEnabled); + } + } + [Fact] public void Opening_Context_Flyout_Does_not_Lose_Selection() { diff --git a/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs b/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs index 4f0446d406..cd38bf556a 100644 --- a/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs +++ b/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs @@ -424,6 +424,587 @@ namespace Avalonia.Controls.UnitTests } } + [Fact] + public void Double_Clicking_Item_Header_Should_Expand_It() + { + using (Application()) + { + var tree = CreateTestTreeData(); + var target = new TreeView + { + Template = CreateTreeViewTemplate(), + Items = tree, + }; + + var visualRoot = new TestRoot(); + visualRoot.Child = target; + + CreateNodeDataTemplate(target); + ApplyTemplates(target); + CollapseAll(target); + + var item = tree[0].Children[1]; + var container = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(item); + + Assert.NotNull(container); + Assert.False(container.IsExpanded); + var header = container.Header as Interactive; + Assert.NotNull(header); + + _mouse.DoubleClick(header); + + Assert.True(container.IsExpanded); + } + } + + [Fact] + public void Double_Clicking_Item_Header_With_No_Children_Does_Not_Expand_It() + { + using (Application()) + { + var tree = CreateTestTreeData(); + var target = new TreeView + { + Template = CreateTreeViewTemplate(), + Items = tree, + }; + + var visualRoot = new TestRoot(); + visualRoot.Child = target; + + CreateNodeDataTemplate(target); + ApplyTemplates(target); + CollapseAll(target); + + var item = tree[0].Children[1].Children[0]; + var container = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(item); + + Assert.NotNull(container); + Assert.False(container.IsExpanded); + var header = container.Header as Interactive; + Assert.NotNull(header); + + _mouse.DoubleClick(header); + + Assert.False(container.IsExpanded); + } + } + + [Fact] + public void Double_Clicking_Item_Header_Should_Collapse_It() + { + using (Application()) + { + var tree = CreateTestTreeData(); + var target = new TreeView + { + Template = CreateTreeViewTemplate(), + Items = tree, + }; + + var visualRoot = new TestRoot(); + visualRoot.Child = target; + + CreateNodeDataTemplate(target); + ApplyTemplates(target); + ExpandAll(target); + + var item = tree[0].Children[1]; + var container = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(item); + + Assert.NotNull(container); + Assert.True(container.IsExpanded); + var header = container.Header as Interactive; + Assert.NotNull(header); + + _mouse.DoubleClick(header); + + Assert.False(container.IsExpanded); + } + } + + [Fact] + public void Enter_Key_Should_Collapse_TreeViewItem() + { + using (Application()) + { + var tree = CreateTestTreeData(); + var target = new TreeView + { + Template = CreateTreeViewTemplate(), + Items = tree, + }; + + var visualRoot = new TestRoot(); + visualRoot.Child = target; + + CreateNodeDataTemplate(target); + ApplyTemplates(target); + ExpandAll(target); // NOTE this line + + var item = tree[0]; + var container = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(item); + + Assert.NotNull(container); + Assert.True(container.IsExpanded); + var header = container.Header as Interactive; + Assert.NotNull(header); + + container.RaiseEvent(new KeyEventArgs + { + RoutedEvent = InputElement.KeyDownEvent, + Key = Key.Enter, + }); + + Assert.False(container.IsExpanded); + } + } + + [Fact] + public void Enter_plus_Ctrl_Key_Should_Collapse_TreeViewItem_Recursively() + { + using (Application()) + { + var tree = CreateTestTreeData(); + var target = new TreeView + { + Template = CreateTreeViewTemplate(), + Items = tree, + }; + + var visualRoot = new TestRoot(); + visualRoot.Child = target; + + CreateNodeDataTemplate(target); + ApplyTemplates(target); + ExpandAll(target); // NOTE this line + + var item = tree[0]; + var container = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(item); + + Assert.NotNull(container); + Assert.True(container.IsExpanded); + var header = container.Header as Interactive; + Assert.NotNull(header); + + container.RaiseEvent(new KeyEventArgs + { + RoutedEvent = InputElement.KeyDownEvent, + Key = Key.Enter, + KeyModifiers = KeyModifiers.Control, + }); + + Assert.False(container.IsExpanded); + + AssertEachItemWithChildrenIsCollapsed(item); + + void AssertEachItemWithChildrenIsCollapsed(Node node) + { + var container = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(node); + Assert.NotNull(container); + if (node.Children?.Count > 0) + { + Assert.False(container.IsExpanded); + foreach (var c in node.Children) + { + AssertEachItemWithChildrenIsCollapsed(c); + } + } + else + { + Assert.True(container.IsExpanded); + } + } + } + } + + [Fact] + public void Enter_Key_Should_Expand_TreeViewItem() + { + using (Application()) + { + var tree = CreateTestTreeData(); + var target = new TreeView + { + Template = CreateTreeViewTemplate(), + Items = tree, + }; + + var visualRoot = new TestRoot(); + visualRoot.Child = target; + + CreateNodeDataTemplate(target); + ApplyTemplates(target); + CollapseAll(target); // NOTE this line + + var item = tree[0]; + var container = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(item); + + Assert.NotNull(container); + Assert.False(container.IsExpanded); + var header = container.Header as Interactive; + Assert.NotNull(header); + + container.RaiseEvent(new KeyEventArgs + { + RoutedEvent = InputElement.KeyDownEvent, + Key = Key.Enter, + }); + + Assert.True(container.IsExpanded); + } + } + + [Fact] + public void Enter_plus_Ctrl_Key_Should_Expand_TreeViewItem_Recursively() + { + using (Application()) + { + var tree = CreateTestTreeData(); + var target = new TreeView + { + Template = CreateTreeViewTemplate(), + Items = tree, + }; + + var visualRoot = new TestRoot(); + visualRoot.Child = target; + + CreateNodeDataTemplate(target); + ApplyTemplates(target); + CollapseAll(target); // NOTE this line + + var item = tree[0]; + var container = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(item); + + Assert.NotNull(container); + Assert.False(container.IsExpanded); + var header = container.Header as Interactive; + Assert.NotNull(header); + + container.RaiseEvent(new KeyEventArgs + { + RoutedEvent = InputElement.KeyDownEvent, + Key = Key.Enter, + KeyModifiers = KeyModifiers.Control, + }); + + Assert.True(container.IsExpanded); + + AssertEachItemWithChildrenIsExpanded(item); + + void AssertEachItemWithChildrenIsExpanded(Node node) + { + var container = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(node); + Assert.NotNull(container); + if (node.Children?.Count > 0) + { + Assert.True(container.IsExpanded); + foreach (var c in node.Children) + { + AssertEachItemWithChildrenIsExpanded(c); + } + } + else + { + Assert.False(container.IsExpanded); + } + } + } + } + + [Fact] + public void Space_Key_Should_Collapse_TreeViewItem() + { + using (Application()) + { + var tree = CreateTestTreeData(); + var target = new TreeView + { + Template = CreateTreeViewTemplate(), + Items = tree, + }; + + var visualRoot = new TestRoot(); + visualRoot.Child = target; + + CreateNodeDataTemplate(target); + ApplyTemplates(target); + ExpandAll(target); // NOTE this line + + var item = tree[0]; + var container = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(item); + + Assert.NotNull(container); + Assert.True(container.IsExpanded); + var header = container.Header as Interactive; + Assert.NotNull(header); + + container.RaiseEvent(new KeyEventArgs + { + RoutedEvent = InputElement.KeyDownEvent, + Key = Key.Enter, + }); + + Assert.False(container.IsExpanded); + } + } + + [Fact] + public void Space_plus_Ctrl_Key_Should_Collapse_TreeViewItem_Recursively() + { + using (Application()) + { + var tree = CreateTestTreeData(); + var target = new TreeView + { + Template = CreateTreeViewTemplate(), + Items = tree, + }; + + var visualRoot = new TestRoot(); + visualRoot.Child = target; + + CreateNodeDataTemplate(target); + ApplyTemplates(target); + ExpandAll(target); // NOTE this line + + var item = tree[0]; + var container = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(item); + + Assert.NotNull(container); + Assert.True(container.IsExpanded); + var header = container.Header as Interactive; + Assert.NotNull(header); + + container.RaiseEvent(new KeyEventArgs + { + RoutedEvent = InputElement.KeyDownEvent, + Key = Key.Enter, + KeyModifiers = KeyModifiers.Control, + }); + + Assert.False(container.IsExpanded); + + AssertEachItemWithChildrenIsCollapsed(item); + + void AssertEachItemWithChildrenIsCollapsed(Node node) + { + var container = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(node); + Assert.NotNull(container); + if (node.Children?.Count > 0) + { + Assert.False(container.IsExpanded); + foreach (var c in node.Children) + { + AssertEachItemWithChildrenIsCollapsed(c); + } + } + else + { + Assert.True(container.IsExpanded); + } + } + } + } + + [Fact] + public void Space_Key_Should_Expand_TreeViewItem() + { + using (Application()) + { + var tree = CreateTestTreeData(); + var target = new TreeView + { + Template = CreateTreeViewTemplate(), + Items = tree, + }; + + var visualRoot = new TestRoot(); + visualRoot.Child = target; + + CreateNodeDataTemplate(target); + ApplyTemplates(target); + CollapseAll(target); // NOTE this line + + var item = tree[0]; + var container = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(item); + + Assert.NotNull(container); + Assert.False(container.IsExpanded); + var header = container.Header as Interactive; + Assert.NotNull(header); + + container.RaiseEvent(new KeyEventArgs + { + RoutedEvent = InputElement.KeyDownEvent, + Key = Key.Enter, + }); + + Assert.True(container.IsExpanded); + } + } + + [Fact] + public void Space_plus_Ctrl_Key_Should_Expand_TreeViewItem_Recursively() + { + using (Application()) + { + var tree = CreateTestTreeData(); + var target = new TreeView + { + Template = CreateTreeViewTemplate(), + Items = tree, + }; + + var visualRoot = new TestRoot(); + visualRoot.Child = target; + + CreateNodeDataTemplate(target); + ApplyTemplates(target); + CollapseAll(target); // NOTE this line + + var item = tree[0]; + var container = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(item); + + Assert.NotNull(container); + Assert.False(container.IsExpanded); + var header = container.Header as Interactive; + Assert.NotNull(header); + + container.RaiseEvent(new KeyEventArgs + { + RoutedEvent = InputElement.KeyDownEvent, + Key = Key.Enter, + KeyModifiers = KeyModifiers.Control, + }); + + Assert.True(container.IsExpanded); + + AssertEachItemWithChildrenIsExpanded(item); + + void AssertEachItemWithChildrenIsExpanded(Node node) + { + var container = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(node); + Assert.NotNull(container); + if (node.Children?.Count > 0) + { + Assert.True(container.IsExpanded); + foreach (var c in node.Children) + { + AssertEachItemWithChildrenIsExpanded(c); + } + } + else + { + Assert.False(container.IsExpanded); + } + } + } + } + + [Fact] + public void Numpad_Star_Should_Expand_All_Children_Recursively() + { + using (Application()) + { + var tree = CreateTestTreeData(); + var target = new TreeView + { + Template = CreateTreeViewTemplate(), + Items = tree, + }; + + var visualRoot = new TestRoot(); + visualRoot.Child = target; + + CreateNodeDataTemplate(target); + ApplyTemplates(target); + CollapseAll(target); + + var item = tree[0]; + var container = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(item); + + Assert.NotNull(container); + container.RaiseEvent(new KeyEventArgs + { + RoutedEvent = InputElement.KeyDownEvent, + Key = Key.Multiply, + }); + + AssertEachItemWithChildrenIsExpanded(item); + + void AssertEachItemWithChildrenIsExpanded(Node node) + { + var container = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(node); + Assert.NotNull(container); + if (node.Children?.Count > 0) + { + Assert.True(container.IsExpanded); + foreach (var c in node.Children) + { + AssertEachItemWithChildrenIsExpanded(c); + } + } + else + { + Assert.False(container.IsExpanded); + } + } + } + } + + [Fact] + public void Numpad_Slash_Should_Collapse_All_Children_Recursively() + { + using (Application()) + { + var tree = CreateTestTreeData(); + var target = new TreeView + { + Template = CreateTreeViewTemplate(), + Items = tree, + }; + + var visualRoot = new TestRoot(); + visualRoot.Child = target; + + CreateNodeDataTemplate(target); + ApplyTemplates(target); + ExpandAll(target); + + var item = tree[0]; + var container = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(item); + + Assert.NotNull(container); + container.RaiseEvent(new KeyEventArgs + { + RoutedEvent = InputElement.KeyDownEvent, + Key = Key.Divide, + }); + + AssertEachItemWithChildrenIsCollapsed(item); + + void AssertEachItemWithChildrenIsCollapsed(Node node) + { + var container = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(node); + Assert.NotNull(container); + if (node.Children?.Count > 0) + { + Assert.False(container.IsExpanded); + foreach (var c in node.Children) + { + AssertEachItemWithChildrenIsCollapsed(c); + } + } + else + { + Assert.True(container.IsExpanded); + } + } + } + } + [Fact] public void Setting_SelectedItem_Should_Set_Container_Selected() { @@ -1313,10 +1894,14 @@ namespace Avalonia.Controls.UnitTests { Children = { - new ContentPresenter + new Border { - Name = "PART_HeaderPresenter", - [~ContentPresenter.ContentProperty] = parent[~TreeViewItem.HeaderProperty], + Name = "PART_Header", + Child = new ContentPresenter + { + Name = "PART_HeaderPresenter", + [~ContentPresenter.ContentProperty] = parent[~TreeViewItem.HeaderProperty], + }.RegisterInNameScope(scope) }.RegisterInNameScope(scope), new ItemsPresenter { @@ -1335,6 +1920,14 @@ namespace Avalonia.Controls.UnitTests } } + private void CollapseAll(TreeView tree) + { + foreach (var i in tree.ItemContainerGenerator.Containers) + { + tree.CollapseSubTree((TreeViewItem)i.ContainerControl); + } + } + private List ExtractItemHeader(TreeView tree, int level) { return ExtractItemContent(tree.Presenter.Panel, 0, level) diff --git a/tests/Avalonia.Controls.UnitTests/WindowTests.cs b/tests/Avalonia.Controls.UnitTests/WindowTests.cs index bb8a683a60..435d0d92ce 100644 --- a/tests/Avalonia.Controls.UnitTests/WindowTests.cs +++ b/tests/Avalonia.Controls.UnitTests/WindowTests.cs @@ -403,7 +403,7 @@ namespace Avalonia.Controls.UnitTests parent.Close(); var ex = Assert.Throws(() => target.Show(parent)); - Assert.Equal("Cannot show a window with a closed parent.", ex.Message); + Assert.Equal("Cannot show a window with a closed owner.", ex.Message); } } @@ -431,7 +431,7 @@ namespace Avalonia.Controls.UnitTests var target = new Window(); var ex = Assert.Throws(() => target.Show(parent)); - Assert.Equal("Cannot show window with non-visible parent.", ex.Message); + Assert.Equal("Cannot show window with non-visible owner.", ex.Message); } } @@ -444,7 +444,7 @@ namespace Avalonia.Controls.UnitTests var target = new Window(); var ex = await Assert.ThrowsAsync(() => target.ShowDialog(parent)); - Assert.Equal("Cannot show window with non-visible parent.", ex.Message); + Assert.Equal("Cannot show window with non-visible owner.", ex.Message); } } @@ -456,7 +456,7 @@ namespace Avalonia.Controls.UnitTests var target = new Window(); var ex = Assert.Throws(() => target.Show(target)); - Assert.Equal("A Window cannot be its own parent.", ex.Message); + Assert.Equal("A Window cannot be its own owner.", ex.Message); } } @@ -986,7 +986,46 @@ namespace Avalonia.Controls.UnitTests Assert.Equal(SizeToContent.WidthAndHeight, target.SizeToContent); } } + + [Fact] + public void IsVisible_Should_Open_Window() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var target = new Window(); + var raised = false; + + target.Opened += (s, e) => raised = true; + target.IsVisible = true; + Assert.True(raised); + } + } + + [Fact] + public void IsVisible_Should_Close_DialogWindow() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var parent = new Window(); + parent.Show(); + + var target = new Window(); + + var raised = false; + + var task = target.ShowDialog(parent); + + target.Closed += (sender, args) => raised = true; + + target.IsVisible = false; + + Assert.True(raised); + + Assert.False(task.Result); + } + } + protected virtual void Show(Window window) { window.Show(); diff --git a/tests/Avalonia.LeakTests/ControlTests.cs b/tests/Avalonia.LeakTests/ControlTests.cs index ae8df6168d..c713a9e61d 100644 --- a/tests/Avalonia.LeakTests/ControlTests.cs +++ b/tests/Avalonia.LeakTests/ControlTests.cs @@ -3,7 +3,7 @@ using System.Collections.Generic; using System.Collections.ObjectModel; using System.Linq; using System.Reactive.Disposables; - +using System.Threading.Tasks; using Avalonia.Collections; using Avalonia.Controls; using Avalonia.Controls.Presenters; @@ -1060,6 +1060,8 @@ namespace Avalonia.LeakTests public void Stop() { } + + public ValueTask TryGetRenderInterfaceFeature(Type featureType) => new(null); } } } diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/MergeResourceIncludeTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/MergeResourceIncludeTests.cs new file mode 100644 index 0000000000..520abee59a --- /dev/null +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/MergeResourceIncludeTests.cs @@ -0,0 +1,124 @@ +using System; +using System.Xml; +using Avalonia.Controls; +using Avalonia.Media; +using Avalonia.Styling; +using Xunit; + +namespace Avalonia.Markup.Xaml.UnitTests.Xaml; + +public class MergeResourceIncludeTests +{ + [Fact] + public void MergeResourceInclude_Works_With_Single_Resource() + { + var documents = new[] + { + new RuntimeXamlLoaderDocument(new Uri("avares://Tests/Resources.xaml"), @" + + Red +"), + new RuntimeXamlLoaderDocument(@" + + + + Blue + + + + + +") + }; + + var objects = AvaloniaRuntimeXamlLoader.LoadGroup(documents); + var contentControl = Assert.IsType(objects[1]); + + var resources = Assert.IsType(contentControl.Resources); + Assert.Empty(resources.MergedDictionaries); + + var initialResource = (ISolidColorBrush)resources["brush1"]!; + Assert.Equal(Colors.Blue, initialResource.Color); + + var mergedResource = (ISolidColorBrush)resources["brush2"]!; + Assert.Equal(Colors.Red, mergedResource.Color); + } + + [Fact] + public void Mixing_MergeResourceInclude_And_ResourceInclude_Is_Not_Allowed() + { + var documents = new[] + { + new RuntimeXamlLoaderDocument(new Uri("avares://Tests/Resources1.xaml"), @" + + Red +"), + new RuntimeXamlLoaderDocument(new Uri("avares://Tests/Resources2.xaml"), @" + + Blue +"), + new RuntimeXamlLoaderDocument(@" + + + + + +") + }; + + Assert.ThrowsAny(() => AvaloniaRuntimeXamlLoader.LoadGroup(documents)); + } + + [Fact] + public void MergeResourceInclude_Works_With_Multiple_Resources() + { + var documents = new[] + { + new RuntimeXamlLoaderDocument(new Uri("avares://Tests/Resources1.xaml"), @" + + Red + Blue +"), + new RuntimeXamlLoaderDocument(new Uri("avares://Tests/Resources2.xaml"), @" + + Yellow + + + +"), + new RuntimeXamlLoaderDocument(@" + + + + + + Black + White +"), + new RuntimeXamlLoaderDocument(new Uri("avares://Tests/Resources1_2.xaml"), @" + + Green +"), + }; + + var objects = AvaloniaRuntimeXamlLoader.LoadGroup(documents); + var resources = Assert.IsType(objects[2]); + Assert.Empty(resources.MergedDictionaries); + + Assert.Equal(Colors.Red, ((ISolidColorBrush)resources["brush1"]!).Color); + Assert.Equal(Colors.Blue, ((ISolidColorBrush)resources["brush2"]!).Color); + Assert.Equal(Colors.Green, ((ISolidColorBrush)resources["brush3"]!).Color); + Assert.Equal(Colors.Yellow, ((ISolidColorBrush)resources["brush4"]!).Color); + Assert.Equal(Colors.Black, ((ISolidColorBrush)resources["brush5"]!).Color); + Assert.Equal(Colors.White, ((ISolidColorBrush)resources["brush6"]!).Color); + } +} diff --git a/tests/Avalonia.RenderTests/Media/BitmapTests.cs b/tests/Avalonia.RenderTests/Media/BitmapTests.cs index d52539c371..2d83f5ce0f 100644 --- a/tests/Avalonia.RenderTests/Media/BitmapTests.cs +++ b/tests/Avalonia.RenderTests/Media/BitmapTests.cs @@ -69,8 +69,9 @@ namespace Avalonia.Direct2D1.RenderTests.Media { var testName = nameof(FramebufferRenderResultsShouldBeUsableAsBitmap) + "_" + fmt; var fb = new Framebuffer(fmt, new PixelSize(80, 80)); - var r = Avalonia.AvaloniaLocator.Current.GetService(); - using (var target = r.CreateRenderTarget(new object[] { fb })) + var r = Avalonia.AvaloniaLocator.Current.GetRequiredService(); + using(var cpuContext = r.CreateBackendContext(null)) + using (var target = cpuContext.CreateRenderTarget(new object[] { fb })) using (var ctx = target.CreateDrawingContext(null)) { ctx.Clear(Colors.Transparent); diff --git a/tests/Avalonia.RenderTests/TestBase.cs b/tests/Avalonia.RenderTests/TestBase.cs index 3f918e2a73..8a127897d7 100644 --- a/tests/Avalonia.RenderTests/TestBase.cs +++ b/tests/Avalonia.RenderTests/TestBase.cs @@ -13,6 +13,7 @@ using System.Collections.Generic; using System.Linq; using System.Reactive.Disposables; using System.Threading; +using Avalonia.Controls.Platform.Surfaces; using Avalonia.Media; using Avalonia.Rendering.Composition; using Avalonia.Threading; @@ -122,10 +123,13 @@ namespace Avalonia.Direct2D1.RenderTests var timer = new ManualRenderTimer(); var compositor = new Compositor(new RenderLoop(timer, Dispatcher.UIThread), null); - using (var rtb = factory.CreateRenderTargetBitmap(pixelSize, dpiVector)) + using (var writableBitmap = factory.CreateWriteableBitmap(pixelSize, dpiVector, factory.DefaultPixelFormat, factory.DefaultAlphaFormat)) { - var root = new TestRenderRoot(dpiVector.X / 96, rtb); - using (var renderer = new CompositingRenderer(root, compositor) { RenderOnlyOnRenderThread = false}) + var root = new TestRenderRoot(dpiVector.X / 96, null!); + using (var renderer = new CompositingRenderer(root, compositor, () => new[] + { + new BitmapFramebufferSurface(writableBitmap) + }) { RenderOnlyOnRenderThread = false }) { root.Initialize(renderer, target); renderer.Start(); @@ -136,8 +140,20 @@ namespace Avalonia.Direct2D1.RenderTests // Free pools for (var c = 0; c < 11; c++) TestThreadingInterface.RunTimers(); - rtb.Save(compositedPath); + writableBitmap.Save(compositedPath); + } + } + + class BitmapFramebufferSurface : IFramebufferPlatformSurface + { + private readonly IWriteableBitmapImpl _bitmap; + + public BitmapFramebufferSurface(IWriteableBitmapImpl bitmap) + { + _bitmap = bitmap; } + + public ILockedFramebuffer Lock() => _bitmap.Lock(); } protected void CompareImages([CallerMemberName] string testName = "") diff --git a/tests/Avalonia.Skia.UnitTests/HitTesting.cs b/tests/Avalonia.Skia.UnitTests/HitTesting.cs index dceb0cdb9b..df267ee136 100644 --- a/tests/Avalonia.Skia.UnitTests/HitTesting.cs +++ b/tests/Avalonia.Skia.UnitTests/HitTesting.cs @@ -30,7 +30,7 @@ namespace Avalonia.Skia.UnitTests } }; - root.Renderer = new DeferredRenderer((IRenderRoot)root, null); + root.Renderer = new DeferredRenderer((IRenderRoot)root, null, root.CreateRenderTarget); root.Measure(Size.Infinity); root.Arrange(new Rect(root.DesiredSize)); @@ -64,7 +64,7 @@ namespace Avalonia.Skia.UnitTests } }; - root.Renderer = new DeferredRenderer((IRenderRoot)root, null); + root.Renderer = new DeferredRenderer((IRenderRoot)root, null, root.CreateRenderTarget); root.Measure(Size.Infinity); root.Arrange(new Rect(root.DesiredSize)); diff --git a/tests/Avalonia.Skia.UnitTests/Media/GlyphRunTests.cs b/tests/Avalonia.Skia.UnitTests/Media/GlyphRunTests.cs index 3b9caa393e..4083a67b5e 100644 --- a/tests/Avalonia.Skia.UnitTests/Media/GlyphRunTests.cs +++ b/tests/Avalonia.Skia.UnitTests/Media/GlyphRunTests.cs @@ -19,7 +19,7 @@ namespace Avalonia.Skia.UnitTests.Media { var options = new TextShaperOptions(Typeface.Default.GlyphTypeface, 10, direction, CultureInfo.CurrentCulture); var shapedBuffer = - TextShaper.Current.ShapeText(text.AsMemory(), options); + TextShaper.Current.ShapeText(new CharacterBufferReference(text), text.Length, options); var glyphRun = CreateGlyphRun(shapedBuffer); @@ -39,8 +39,6 @@ namespace Avalonia.Skia.UnitTests.Media } else { - shapedBuffer.GlyphInfos.Span.Reverse(); - foreach (var rect in rects) { characterHit = glyphRun.GetNextCaretCharacterHit(characterHit); @@ -62,7 +60,7 @@ namespace Avalonia.Skia.UnitTests.Media { var options = new TextShaperOptions(Typeface.Default.GlyphTypeface, 10, direction, CultureInfo.CurrentCulture); var shapedBuffer = - TextShaper.Current.ShapeText(text.AsMemory(), options); + TextShaper.Current.ShapeText(new CharacterBufferReference(text), text.Length, options); var glyphRun = CreateGlyphRun(shapedBuffer); @@ -84,8 +82,6 @@ namespace Avalonia.Skia.UnitTests.Media } else { - shapedBuffer.GlyphInfos.Span.Reverse(); - foreach (var rect in rects) { characterHit = glyphRun.GetPreviousCaretCharacterHit(characterHit); @@ -107,7 +103,7 @@ namespace Avalonia.Skia.UnitTests.Media { var options = new TextShaperOptions(Typeface.Default.GlyphTypeface, 10, direction, CultureInfo.CurrentCulture); var shapedBuffer = - TextShaper.Current.ShapeText(text.AsMemory(), options); + TextShaper.Current.ShapeText(new CharacterBufferReference(text), text.Length, options); var glyphRun = CreateGlyphRun(shapedBuffer); @@ -116,16 +112,14 @@ namespace Avalonia.Skia.UnitTests.Media var characterHit = glyphRun.GetCharacterHitFromDistance(glyphRun.Metrics.WidthIncludingTrailingWhitespace, out _); - Assert.Equal(glyphRun.Characters.Length, characterHit.FirstCharacterIndex + characterHit.TrailingLength); + Assert.Equal(glyphRun.Characters.Count, characterHit.FirstCharacterIndex + characterHit.TrailingLength); } else { - shapedBuffer.GlyphInfos.Span.Reverse(); - - var characterHit = + var characterHit = glyphRun.GetCharacterHitFromDistance(0, out _); - Assert.Equal(glyphRun.Characters.Length, characterHit.FirstCharacterIndex + characterHit.TrailingLength); + Assert.Equal(glyphRun.Characters.Count, characterHit.FirstCharacterIndex + characterHit.TrailingLength); } var rects = BuildRects(glyphRun); @@ -218,15 +212,22 @@ namespace Avalonia.Skia.UnitTests.Media private static GlyphRun CreateGlyphRun(ShapedBuffer shapedBuffer) { - return new GlyphRun( + var glyphRun = new GlyphRun( shapedBuffer.GlyphTypeface, shapedBuffer.FontRenderingEmSize, - shapedBuffer.Text, + shapedBuffer.CharacterBufferRange, shapedBuffer.GlyphIndices, shapedBuffer.GlyphAdvances, shapedBuffer.GlyphOffsets, shapedBuffer.GlyphClusters, shapedBuffer.BidiLevel); + + if(shapedBuffer.BidiLevel == 1) + { + shapedBuffer.GlyphInfos.Span.Reverse(); + } + + return glyphRun; } private static IDisposable Start() diff --git a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/MultiBufferTextSource.cs b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/MultiBufferTextSource.cs index 005bcdf70e..aa499bb135 100644 --- a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/MultiBufferTextSource.cs +++ b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/MultiBufferTextSource.cs @@ -29,8 +29,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting var runText = _runTexts[index]; - return new TextCharacters( - new ReadOnlySlice(runText.AsMemory(), textSourceIndex, runText.Length), _defaultStyle); + return new TextCharacters(runText, _defaultStyle); } } } diff --git a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/SingleBufferTextSource.cs b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/SingleBufferTextSource.cs index dee4fe7f77..f12f42bd5e 100644 --- a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/SingleBufferTextSource.cs +++ b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/SingleBufferTextSource.cs @@ -1,30 +1,33 @@ -using System; -using Avalonia.Media.TextFormatting; -using Avalonia.Utilities; +using Avalonia.Media.TextFormatting; namespace Avalonia.Skia.UnitTests.Media.TextFormatting { internal class SingleBufferTextSource : ITextSource { - private readonly ReadOnlySlice _text; + private readonly CharacterBufferRange _text; private readonly GenericTextRunProperties _defaultGenericPropertiesRunProperties; public SingleBufferTextSource(string text, GenericTextRunProperties defaultProperties) { - _text = text.AsMemory(); + _text = new CharacterBufferRange(text); _defaultGenericPropertiesRunProperties = defaultProperties; } public TextRun GetTextRun(int textSourceIndex) { - if (textSourceIndex > _text.Length) + if (textSourceIndex >= _text.Length) { return null; } - + var runText = _text.Skip(textSourceIndex); - return runText.IsEmpty ? null : new TextCharacters(runText, _defaultGenericPropertiesRunProperties); + if (runText.IsEmpty) + { + return null; + } + + return new TextCharacters(runText.CharacterBufferReference, runText.Length, _defaultGenericPropertiesRunProperties); } } } diff --git a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs index 316926b00c..33d4fba5f1 100644 --- a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs +++ b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs @@ -37,7 +37,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting Assert.Equal(defaultProperties.ForegroundBrush, textRun.Properties.ForegroundBrush); - Assert.Equal(text.Length, textRun.Text.Length); + Assert.Equal(text.Length, textRun.Length); } } @@ -82,7 +82,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting new ValueSpan(9, 1, defaultProperties) }; - var textSource = new FormattedTextSource(text.AsMemory(), defaultProperties, GenericTextRunPropertiesRuns); + var textSource = new FormattedTextSource(text, defaultProperties, GenericTextRunPropertiesRuns); var formatter = new TextFormatterImpl(); @@ -97,7 +97,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting var textRun = textLine.TextRuns[i]; - Assert.Equal(GenericTextRunPropertiesRun.Length, textRun.Text.Length); + Assert.Equal(GenericTextRunPropertiesRun.Length, textRun.Length); } } } @@ -166,7 +166,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting var firstRun = textLine.TextRuns[0]; - Assert.Equal(4, firstRun.Text.Length); + Assert.Equal(4, firstRun.Length); } } @@ -216,7 +216,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting { using (Start()) { - var lineBreaker = new LineBreakEnumerator(text.AsMemory()); + var lineBreaker = new LineBreakEnumerator(new CharacterBufferRange(text)); var expected = new List(); @@ -369,7 +369,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting new GenericTextRunProperties(new Typeface("Verdana", FontStyle.Italic),32)) }; - var textSource = new FormattedTextSource(text.AsMemory(), defaultProperties, styleSpans); + var textSource = new FormattedTextSource(text, defaultProperties, styleSpans); var formatter = new TextFormatterImpl(); @@ -389,7 +389,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting if (textLine.Width > 300 || currentHeight + textLine.Height > 240) { - textLine = textLine.Collapse(new TextTrailingWordEllipsis(new ReadOnlySlice(new[] { TextTrimming.DefaultEllipsisChar }), 300, defaultProperties)); + textLine = textLine.Collapse(new TextTrailingWordEllipsis(TextTrimming.DefaultEllipsisChar, 300, defaultProperties)); } currentHeight += textLine.Height; @@ -472,7 +472,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting var textLine = formatter.FormatLine(textSource, textPosition, 50, paragraphProperties, lastBreak); - Assert.Equal(textLine.Length, textLine.TextRuns.Sum(x => x.TextSourceLength)); + Assert.Equal(textLine.Length, textLine.TextRuns.Sum(x => x.Length)); textPosition += textLine.Length; @@ -534,7 +534,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting new GenericTextRunProperties(Typeface.Default, 12, foregroundBrush: foreground)) }; - var textSource = new FormattedTextSource(text.AsMemory(), defaultProperties, spans); + var textSource = new FormattedTextSource(text, defaultProperties, spans); var textLine = formatter.FormatLine(textSource, 0, double.PositiveInfinity, paragraphProperties); @@ -614,8 +614,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting return new RectangleRun(new Rect(0, 0, 50, 50), Brushes.Green); } - return new TextCharacters(_text.AsMemory(), - new GenericTextRunProperties(Typeface.Default, foregroundBrush: Brushes.Black)); + return new TextCharacters(_text, 0, _text.Length, new GenericTextRunProperties(Typeface.Default, foregroundBrush: Brushes.Black)); } } diff --git a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs index d6da2c77c4..a407b38eb1 100644 --- a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs +++ b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs @@ -60,9 +60,9 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting var textRun = textLine.TextRuns[1]; - Assert.Equal(2, textRun.Text.Length); + Assert.Equal(2, textRun.Length); - var actual = textRun.Text.Span.ToString(); + var actual = new CharacterBufferRange(textRun).Span.ToString(); Assert.Equal("1 ", actual); @@ -144,8 +144,8 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting var expectedGlyphs = expected.TextLines.Select(x => string.Join('|', x.TextRuns.Cast() .SelectMany(x => x.ShapedBuffer.GlyphIndices))).ToList(); - var outer = new GraphemeEnumerator(text.AsMemory()); - var inner = new GraphemeEnumerator(text.AsMemory()); + var outer = new GraphemeEnumerator(new CharacterBufferRange(text)); + var inner = new GraphemeEnumerator(new CharacterBufferRange(text)); var i = 0; var j = 0; @@ -190,7 +190,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting break; } - inner = new GraphemeEnumerator(text.AsMemory()); + inner = new GraphemeEnumerator(new CharacterBufferRange(text)); i += outer.Current.Text.Length; } @@ -223,10 +223,9 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting var textRun = textLine.TextRuns[0]; - Assert.Equal(2, textRun.Text.Length); + Assert.Equal(2, textRun.Length); - var actual = SingleLineText.Substring(textRun.Text.Start, - textRun.Text.Length); + var actual = SingleLineText[..textRun.Length]; Assert.Equal("01", actual); @@ -260,9 +259,9 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting var textRun = textLine.TextRuns[1]; - Assert.Equal(2, textRun.Text.Length); + Assert.Equal(2, textRun.Length); - var actual = textRun.Text.Span.ToString(); + var actual = new CharacterBufferRange(textRun).Span.ToString(); Assert.Equal("89", actual); @@ -296,7 +295,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting var textRun = textLine.TextRuns[0]; - Assert.Equal(1, textRun.Text.Length); + Assert.Equal(1, textRun.Length); Assert.Equal(foreground, textRun.Properties.ForegroundBrush); } @@ -330,9 +329,9 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting var textRun = textLine.TextRuns[1]; - Assert.Equal(2, textRun.Text.Length); + Assert.Equal(2, textRun.Length); - var actual = textRun.Text.Span.ToString(); + var actual = new CharacterBufferRange(textRun).Span.ToString(); Assert.Equal("😄", actual); @@ -369,7 +368,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting Assert.Equal( MultiLineText.Length, layout.TextLines.Select(textLine => - textLine.TextRuns.Sum(textRun => textRun.Text.Length)) + textLine.TextRuns.Sum(textRun => textRun.Length)) .Sum()); } } @@ -402,7 +401,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting Assert.Equal( text.Length, layout.TextLines.Select(textLine => - textLine.TextRuns.Sum(textRun => textRun.Text.Length)) + textLine.TextRuns.Sum(textRun => textRun.Length)) .Sum()); } } @@ -558,7 +557,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting var textRun = (ShapedTextCharacters)textLine.TextRuns[0]; - Assert.Equal(7, textRun.Text.Length); + Assert.Equal(7, textRun.Length); var replacementGlyph = Typeface.Default.GlyphTypeface.GetGlyph(Codepoint.ReplacementCodepoint); @@ -668,10 +667,10 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting Assert.Equal(5, layout.TextLines.Count); - Assert.Equal("123\r\n", layout.TextLines[0].TextRuns[0].Text); - Assert.Equal("\r\n", layout.TextLines[1].TextRuns[0].Text); - Assert.Equal("456\r\n", layout.TextLines[2].TextRuns[0].Text); - Assert.Equal("\r\n", layout.TextLines[3].TextRuns[0].Text); + Assert.Equal("123\r\n", new CharacterBufferRange(layout.TextLines[0].TextRuns[0])); + Assert.Equal("\r\n", new CharacterBufferRange(layout.TextLines[1].TextRuns[0])); + Assert.Equal("456\r\n", new CharacterBufferRange(layout.TextLines[2].TextRuns[0])); + Assert.Equal("\r\n", new CharacterBufferRange(layout.TextLines[3].TextRuns[0])); } } @@ -815,7 +814,10 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting { Assert.True(textLine.Width <= maxWidth); - var actual = new string(textLine.TextRuns.Cast().OrderBy(x => x.Text.Start).SelectMany(x => x.Text).ToArray()); + var actual = new string(textLine.TextRuns.Cast() + .OrderBy(x => x.CharacterBufferReference.OffsetToFirstChar) + .SelectMany(x => new CharacterBufferRange(x.CharacterBufferReference, x.Length)).ToArray()); + var expected = text.Substring(textLine.FirstTextSourceIndex, textLine.Length); Assert.Equal(expected, actual); @@ -966,7 +968,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting var i = 0; - var graphemeEnumerator = new GraphemeEnumerator(text.AsMemory()); + var graphemeEnumerator = new GraphemeEnumerator(new CharacterBufferRange(text)); while (graphemeEnumerator.MoveNext()) { diff --git a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs index 87de9ed11f..d6257a0de8 100644 --- a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs +++ b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs @@ -90,7 +90,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting var clusters = new List(); - foreach (var textRun in textLine.TextRuns.OrderBy(x => x.Text.Start)) + foreach (var textRun in textLine.TextRuns.OrderBy(x => x.CharacterBufferReference.OffsetToFirstChar)) { var shapedRun = (ShapedTextCharacters)textRun; @@ -137,7 +137,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting var clusters = new List(); - foreach (var textRun in textLine.TextRuns.OrderBy(x => x.Text.Start)) + foreach (var textRun in textLine.TextRuns.OrderBy(x => x.CharacterBufferReference.OffsetToFirstChar)) { var shapedRun = (ShapedTextCharacters)textRun; @@ -187,14 +187,16 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting formatter.FormatLine(textSource, 0, double.PositiveInfinity, new GenericTextParagraphProperties(defaultProperties)); - var clusters = textLine.TextRuns.Cast().SelectMany(x => x.ShapedBuffer.GlyphClusters) - .ToArray(); + var clusters = BuildGlyphClusters(textLine); var nextCharacterHit = new CharacterHit(0); - for (var i = 0; i < clusters.Length; i++) + for (var i = 0; i < clusters.Count; i++) { - Assert.Equal(clusters[i], nextCharacterHit.FirstCharacterIndex); + var expectedCluster = clusters[i]; + var actualCluster = nextCharacterHit.FirstCharacterIndex; + + Assert.Equal(expectedCluster, actualCluster); nextCharacterHit = textLine.GetNextCaretCharacterHit(nextCharacterHit); } @@ -406,7 +408,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting Assert.True(collapsedLine.HasCollapsed); - var trimmedText = collapsedLine.TextRuns.SelectMany(x => x.Text).ToArray(); + var trimmedText = collapsedLine.TextRuns.SelectMany(x => new CharacterBufferRange(x)).ToArray(); Assert.Equal(expected.Length, trimmedText.Length); @@ -450,8 +452,8 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting currentHit = textLine.GetNextCaretCharacterHit(currentHit); - Assert.Equal(3, currentHit.FirstCharacterIndex); - Assert.Equal(1, currentHit.TrailingLength); + Assert.Equal(4, currentHit.FirstCharacterIndex); + Assert.Equal(0, currentHit.TrailingLength); } } @@ -473,18 +475,18 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting var currentHit = textLine.GetPreviousCaretCharacterHit(new CharacterHit(3, 1)); - Assert.Equal(3, currentHit.FirstCharacterIndex); - Assert.Equal(0, currentHit.TrailingLength); + Assert.Equal(2, currentHit.FirstCharacterIndex); + Assert.Equal(1, currentHit.TrailingLength); currentHit = textLine.GetPreviousCaretCharacterHit(currentHit); - Assert.Equal(2, currentHit.FirstCharacterIndex); - Assert.Equal(0, currentHit.TrailingLength); + Assert.Equal(1, currentHit.FirstCharacterIndex); + Assert.Equal(1, currentHit.TrailingLength); currentHit = textLine.GetPreviousCaretCharacterHit(currentHit); - Assert.Equal(1, currentHit.FirstCharacterIndex); - Assert.Equal(0, currentHit.TrailingLength); + Assert.Equal(0, currentHit.FirstCharacterIndex); + Assert.Equal(1, currentHit.TrailingLength); currentHit = textLine.GetPreviousCaretCharacterHit(currentHit); @@ -509,13 +511,13 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting var characterHit = textLine.GetCharacterHitFromDistance(50); - Assert.Equal(3, characterHit.FirstCharacterIndex); + Assert.Equal(5, characterHit.FirstCharacterIndex); Assert.Equal(1, characterHit.TrailingLength); characterHit = textLine.GetCharacterHitFromDistance(32); - Assert.Equal(2, characterHit.FirstCharacterIndex); - Assert.Equal(1, characterHit.TrailingLength); + Assert.Equal(3, characterHit.FirstCharacterIndex); + Assert.Equal(0, characterHit.TrailingLength); } } @@ -649,7 +651,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting var run = textRuns[i]; var bounds = runBounds[i]; - Assert.Equal(run.Text.Start, bounds.TextSourceCharacterIndex); + Assert.Equal(run.CharacterBufferReference.OffsetToFirstChar, bounds.TextSourceCharacterIndex); Assert.Equal(run, bounds.TextRun); Assert.Equal(run.Size.Width, bounds.Rectangle.Width); } @@ -683,13 +685,13 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting switch (textSourceIndex) { case 0: - return new TextCharacters(new ReadOnlySlice("aaaaaaaaaa".AsMemory()), new GenericTextRunProperties(Typeface.Default)); + return new TextCharacters("aaaaaaaaaa", new GenericTextRunProperties(Typeface.Default)); case 10: - return new TextCharacters(new ReadOnlySlice("bbbbbbbbbb".AsMemory()), new GenericTextRunProperties(Typeface.Default)); + return new TextCharacters("bbbbbbbbbb", new GenericTextRunProperties(Typeface.Default)); case 20: - return new TextCharacters(new ReadOnlySlice("cccccccccc".AsMemory()), new GenericTextRunProperties(Typeface.Default)); + return new TextCharacters("cccccccccc", new GenericTextRunProperties(Typeface.Default)); case 30: - return new TextCharacters(new ReadOnlySlice("dddddddddd".AsMemory()), new GenericTextRunProperties(Typeface.Default)); + return new TextCharacters("dddddddddd", new GenericTextRunProperties(Typeface.Default)); default: return null; } @@ -698,7 +700,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting private class DrawableRunTextSource : ITextSource { - const string Text = "_A_A"; + private const string Text = "_A_A"; public TextRun GetTextRun(int textSourceIndex) { @@ -707,11 +709,11 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting case 0: return new CustomDrawableRun(); case 1: - return new TextCharacters(new ReadOnlySlice(Text.AsMemory(), 1, 1, 1), new GenericTextRunProperties(Typeface.Default)); - case 2: + return new TextCharacters(Text, new GenericTextRunProperties(Typeface.Default)); + case 5: return new CustomDrawableRun(); - case 3: - return new TextCharacters(new ReadOnlySlice(Text.AsMemory(), 3, 1, 3), new GenericTextRunProperties(Typeface.Default)); + case 6: + return new TextCharacters(Text, new GenericTextRunProperties(Typeface.Default)); default: return null; } @@ -815,19 +817,19 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting using (Start()) { var defaultProperties = new GenericTextRunProperties(Typeface.Default); - var text = "0123".AsMemory(); + var text = "0123"; var shaperOption = new TextShaperOptions(Typeface.Default.GlyphTypeface, 10, 0, CultureInfo.CurrentCulture); - var firstRun = new ShapedTextCharacters(TextShaper.Current.ShapeText(new ReadOnlySlice(text, 1, text.Length), shaperOption), defaultProperties); + var firstRun = new ShapedTextCharacters(TextShaper.Current.ShapeText(text, shaperOption), defaultProperties); var textRuns = new List { new CustomDrawableRun(), firstRun, new CustomDrawableRun(), - new ShapedTextCharacters(TextShaper.Current.ShapeText(new ReadOnlySlice(text, text.Length + 2, text.Length), shaperOption), defaultProperties), + new ShapedTextCharacters(TextShaper.Current.ShapeText(text, shaperOption), defaultProperties), new CustomDrawableRun(), - new ShapedTextCharacters(TextShaper.Current.ShapeText(new ReadOnlySlice(text, text.Length * 2 + 3, text.Length), shaperOption), defaultProperties) + new ShapedTextCharacters(TextShaper.Current.ShapeText(text, shaperOption), defaultProperties) }; var textSource = new FixedRunsTextSource(textRuns); @@ -838,7 +840,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting formatter.FormatLine(textSource, 0, double.PositiveInfinity, new GenericTextParagraphProperties(defaultProperties)); - var textBounds = textLine.GetTextBounds(0, text.Length * 3 + 3); + var textBounds = textLine.GetTextBounds(0, textLine.Length); Assert.Equal(6, textBounds.Count); Assert.Equal(textLine.WidthIncludingTrailingWhitespace, textBounds.Sum(x => x.Rectangle.Width)); @@ -848,17 +850,17 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting Assert.Equal(1, textBounds.Count); Assert.Equal(14, textBounds[0].Rectangle.Width); - textBounds = textLine.GetTextBounds(0, firstRun.Text.Length + 1); + textBounds = textLine.GetTextBounds(0, firstRun.Length + 1); Assert.Equal(2, textBounds.Count); Assert.Equal(firstRun.Size.Width + 14, textBounds.Sum(x => x.Rectangle.Width)); - textBounds = textLine.GetTextBounds(1, firstRun.Text.Length); + textBounds = textLine.GetTextBounds(1, firstRun.Length); Assert.Equal(1, textBounds.Count); Assert.Equal(firstRun.Size.Width, textBounds[0].Rectangle.Width); - textBounds = textLine.GetTextBounds(1, firstRun.Text.Length + 1); + textBounds = textLine.GetTextBounds(0, 1 + firstRun.Length); Assert.Equal(2, textBounds.Count); Assert.Equal(firstRun.Size.Width + 14, textBounds.Sum(x => x.Rectangle.Width)); @@ -878,7 +880,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting var textLine = formatter.FormatLine(textSource, 0, 200, - new GenericTextParagraphProperties(FlowDirection.LeftToRight, TextAlignment.Left, + new GenericTextParagraphProperties(FlowDirection.LeftToRight, TextAlignment.Left, true, true, defaultProperties, TextWrapping.NoWrap, 0, 0, 0)); var textBounds = textLine.GetTextBounds(0, 3); @@ -899,11 +901,11 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting Assert.Equal(2, textBounds.Count); - Assert.Equal(firstRun.Size.Width, textBounds[0].Rectangle.Width); + Assert.Equal(firstRun.Size.Width, textBounds[0].Rectangle.Width); Assert.Equal(7.201171875, textBounds[1].Rectangle.Width); - Assert.Equal(firstRun.Size.Width, textBounds[1].Rectangle.Left); + Assert.Equal(firstRun.Size.Width, textBounds[1].Rectangle.Left); textBounds = textLine.GetTextBounds(0, text.Length); @@ -925,7 +927,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting var textLine = formatter.FormatLine(textSource, 0, 200, - new GenericTextParagraphProperties(FlowDirection.RightToLeft, TextAlignment.Left, + new GenericTextParagraphProperties(FlowDirection.RightToLeft, TextAlignment.Left, true, true, defaultProperties, TextWrapping.NoWrap, 0, 0, 0)); var textBounds = textLine.GetTextBounds(0, 4); @@ -941,13 +943,13 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting Assert.Equal(1, textBounds.Count); - Assert.Equal(3, textBounds[0].TextRunBounds.Sum(x=> x.Length)); + Assert.Equal(3, textBounds[0].TextRunBounds.Sum(x => x.Length)); Assert.Equal(firstRun.Size.Width, textBounds.Sum(x => x.Rectangle.Width)); textBounds = textLine.GetTextBounds(0, 5); Assert.Equal(2, textBounds.Count); - Assert.Equal(5, textBounds.Sum(x=> x.TextRunBounds.Sum(x => x.Length))); + Assert.Equal(5, textBounds.Sum(x => x.TextRunBounds.Sum(x => x.Length))); Assert.Equal(secondRun.Size.Width, textBounds[1].Rectangle.Width); Assert.Equal(7.201171875, textBounds[0].Rectangle.Width); @@ -960,7 +962,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting Assert.Equal(7, textBounds.Sum(x => x.TextRunBounds.Sum(x => x.Length))); Assert.Equal(textLine.WidthIncludingTrailingWhitespace, textBounds.Sum(x => x.Rectangle.Width)); } - } + } private class FixedRunsTextSource : ITextSource { @@ -982,7 +984,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting return textRun; } - currentPosition += textRun.TextSourceLength; + currentPosition += textRun.Length; } return null; diff --git a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextShaperTests.cs b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextShaperTests.cs index 94933e334d..63e0083b1d 100644 --- a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextShaperTests.cs +++ b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextShaperTests.cs @@ -14,11 +14,11 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting { using (Start()) { - var text = "\n\r\n".AsMemory(); + var text = "\n\r\n"; var options = new TextShaperOptions(Typeface.Default.GlyphTypeface, 12,0, CultureInfo.CurrentCulture); var shapedBuffer = TextShaper.Current.ShapeText(text, options); - Assert.Equal(shapedBuffer.Text.Length, text.Length); + Assert.Equal(shapedBuffer.CharacterBufferRange.Length, text.Length); Assert.Equal(shapedBuffer.GlyphClusters.Count, text.Length); Assert.Equal(0, shapedBuffer.GlyphClusters[0]); Assert.Equal(1, shapedBuffer.GlyphClusters[1]); @@ -31,7 +31,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting { using (Start()) { - var text = "\t".AsMemory(); + var text = "\t"; var options = new TextShaperOptions(Typeface.Default.GlyphTypeface, 12, 0, CultureInfo.CurrentCulture, 100); var shapedBuffer = TextShaper.Current.ShapeText(text, options); diff --git a/tests/Avalonia.UnitTests/HarfBuzzTextShaperImpl.cs b/tests/Avalonia.UnitTests/HarfBuzzTextShaperImpl.cs index 7b7488bd5a..ae7e00aca1 100644 --- a/tests/Avalonia.UnitTests/HarfBuzzTextShaperImpl.cs +++ b/tests/Avalonia.UnitTests/HarfBuzzTextShaperImpl.cs @@ -11,7 +11,7 @@ namespace Avalonia.UnitTests { public class HarfBuzzTextShaperImpl : ITextShaperImpl { - public ShapedBuffer ShapeText(ReadOnlySlice text, TextShaperOptions options) + public ShapedBuffer ShapeText(CharacterBufferReference text, int textLength, TextShaperOptions options) { var typeface = options.Typeface; var fontRenderingEmSize = options.FontRenderingEmSize; @@ -20,7 +20,7 @@ namespace Avalonia.UnitTests using (var buffer = new Buffer()) { - buffer.AddUtf16(text.Buffer.Span, text.Start, text.Length); + buffer.AddUtf16(text.CharacterBuffer.Span, text.OffsetToFirstChar, textLength); MergeBreakPair(buffer); @@ -45,7 +45,9 @@ namespace Avalonia.UnitTests var bufferLength = buffer.Length; - var shapedBuffer = new ShapedBuffer(text, bufferLength, typeface, fontRenderingEmSize, bidiLevel); + var characterBufferRange = new CharacterBufferRange(text, textLength); + + var shapedBuffer = new ShapedBuffer(characterBufferRange, bufferLength, typeface, fontRenderingEmSize, bidiLevel); var glyphInfos = buffer.GetGlyphInfoSpan(); diff --git a/tests/Avalonia.UnitTests/MockPlatformRenderInterface.cs b/tests/Avalonia.UnitTests/MockPlatformRenderInterface.cs index 0f951ed867..f9e1e45098 100644 --- a/tests/Avalonia.UnitTests/MockPlatformRenderInterface.cs +++ b/tests/Avalonia.UnitTests/MockPlatformRenderInterface.cs @@ -9,7 +9,7 @@ using Moq; namespace Avalonia.UnitTests { - public class MockPlatformRenderInterface : IPlatformRenderInterface + public class MockPlatformRenderInterface : IPlatformRenderInterface, IPlatformRenderInterfaceContext { public IGeometryImpl CreateEllipseGeometry(Rect rect) { @@ -48,6 +48,8 @@ namespace Avalonia.UnitTests return m.Object; } + + public bool IsCorrupted => false; } public IRenderTarget CreateRenderTarget(IEnumerable surfaces) @@ -55,6 +57,8 @@ namespace Avalonia.UnitTests return new MockRenderTarget(); } + public object TryGetFeature(Type featureType) => null; + public IRenderTargetBitmapImpl CreateRenderTargetBitmap(PixelSize size, Vector dpi) { return Mock.Of(); @@ -147,6 +151,8 @@ namespace Avalonia.UnitTests return Mock.Of(); } + public IPlatformRenderInterfaceContext CreateBackendContext(IPlatformGraphicsContext graphicsContext) => this; + public IGeometryImpl BuildGlyphRunGeometry(GlyphRun glyphRun) { return Mock.Of(); @@ -172,5 +178,8 @@ namespace Avalonia.UnitTests public AlphaFormat DefaultAlphaFormat => AlphaFormat.Premul; public PixelFormat DefaultPixelFormat => PixelFormat.Rgba8888; + public void Dispose() + { + } } } diff --git a/tests/Avalonia.UnitTests/MockTextShaperImpl.cs b/tests/Avalonia.UnitTests/MockTextShaperImpl.cs index 7c34bd192e..00bcef295a 100644 --- a/tests/Avalonia.UnitTests/MockTextShaperImpl.cs +++ b/tests/Avalonia.UnitTests/MockTextShaperImpl.cs @@ -1,24 +1,24 @@ using Avalonia.Media.TextFormatting; using Avalonia.Media.TextFormatting.Unicode; using Avalonia.Platform; -using Avalonia.Utilities; namespace Avalonia.UnitTests { public class MockTextShaperImpl : ITextShaperImpl { - public ShapedBuffer ShapeText(ReadOnlySlice text, TextShaperOptions options) + public ShapedBuffer ShapeText(CharacterBufferReference text, int length, TextShaperOptions options) { var typeface = options.Typeface; var fontRenderingEmSize = options.FontRenderingEmSize; var bidiLevel = options.BidiLevel; - - var shapedBuffer = new ShapedBuffer(text, text.Length, typeface, fontRenderingEmSize, bidiLevel); + var characterBufferRange = new CharacterBufferRange(text, length); + var shapedBuffer = new ShapedBuffer(characterBufferRange, length, typeface, fontRenderingEmSize, bidiLevel); for (var i = 0; i < shapedBuffer.Length;) { - var glyphCluster = i + text.Start; - var codepoint = Codepoint.ReadAt(text, i, out var count); + var glyphCluster = i + text.OffsetToFirstChar; + + var codepoint = Codepoint.ReadAt(characterBufferRange, i, out var count); var glyphIndex = typeface.GetGlyph(codepoint); diff --git a/tests/Avalonia.UnitTests/MouseTestHelper.cs b/tests/Avalonia.UnitTests/MouseTestHelper.cs index c9e4274d15..d63327239b 100644 --- a/tests/Avalonia.UnitTests/MouseTestHelper.cs +++ b/tests/Avalonia.UnitTests/MouseTestHelper.cs @@ -65,6 +65,7 @@ namespace Avalonia.UnitTests } public void Move(Interactive target, in Point position, KeyModifiers modifiers = default) => Move(target, target, position, modifiers); + public void Move(Interactive target, Interactive source, in Point position, KeyModifiers modifiers = default) { target.RaiseEvent(new PointerEventArgs(InputElement.PointerMovedEvent, source, _pointer, (Visual)target, position, @@ -98,13 +99,26 @@ namespace Avalonia.UnitTests public void Click(Interactive target, MouseButton button = MouseButton.Left, Point position = default, KeyModifiers modifiers = default) => Click(target, target, button, position, modifiers); + public void Click(Interactive target, Interactive source, MouseButton button = MouseButton.Left, Point position = default, KeyModifiers modifiers = default) { Down(target, source, button, position, modifiers); Up(target, source, button, position, modifiers); } - + + public void DoubleClick(Interactive target, MouseButton button = MouseButton.Left, Point position = default, + KeyModifiers modifiers = default) + => DoubleClick(target, target, button, position, modifiers); + + public void DoubleClick(Interactive target, Interactive source, MouseButton button = MouseButton.Left, + Point position = default, KeyModifiers modifiers = default) + { + Down(target, source, button, position, modifiers, clickCount: 1); + Up(target, source, button, position, modifiers); + Down(target, source, button, position, modifiers, clickCount: 2); + } + public void Enter(Interactive target) { target.RaiseEvent(new PointerEventArgs(InputElement.PointerEnteredEvent, target, _pointer, (Visual)target, default,