Browse Source

Merge branch 'master' into feature/avaloniaproperty-analyzer

pull/10244/head
Tom Edwards 4 years ago
committed by GitHub
parent
commit
6584bac030
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 17
      native/Avalonia.Native/src/OSX/AvnWindow.mm
  2. 2
      native/Avalonia.Native/src/OSX/PopupImpl.mm
  3. 5
      native/Avalonia.Native/src/OSX/WindowBaseImpl.h
  4. 29
      native/Avalonia.Native/src/OSX/WindowBaseImpl.mm
  5. 6
      native/Avalonia.Native/src/OSX/WindowImpl.h
  6. 101
      native/Avalonia.Native/src/OSX/WindowImpl.mm
  7. 11
      samples/ControlCatalog.Browser.Blazor/App.razor.cs
  8. 12
      samples/ControlCatalog.Browser.Blazor/Program.cs
  9. 20
      samples/ControlCatalog.Browser/Program.cs
  10. 3
      samples/ControlCatalog.Browser/main.js
  11. 51
      samples/ControlCatalog/Pages/DialogsPage.xaml.cs
  12. 4
      samples/IntegrationTestApp/MainWindow.axaml
  13. 2
      samples/IntegrationTestApp/MainWindow.axaml.cs
  14. 6
      src/Android/Avalonia.Android/Platform/Storage/AndroidStorageItem.cs
  15. 6
      src/Android/Avalonia.Android/Platform/Storage/AndroidStorageProvider.cs
  16. 10
      src/Avalonia.Base/AvaloniaObject.cs
  17. 4
      src/Avalonia.Base/Data/Core/IndexerNodeBase.cs
  18. 4
      src/Avalonia.Base/Data/Core/Plugins/InpcPropertyAccessorPlugin.cs
  19. 27
      src/Avalonia.Base/Layout/LayoutInformation.cs
  20. 3
      src/Avalonia.Base/Layout/Layoutable.cs
  21. 2
      src/Avalonia.Base/Platform/DefaultPlatformSettings.cs
  22. 6
      src/Avalonia.Base/Platform/Storage/FileIO/BclStorageFile.cs
  23. 6
      src/Avalonia.Base/Platform/Storage/FileIO/BclStorageProvider.cs
  24. 16
      src/Avalonia.Base/Platform/Storage/FilePickerFileType.cs
  25. 12
      src/Avalonia.Base/Platform/Storage/IStorageFile.cs
  26. 6
      src/Avalonia.Base/Platform/Storage/IStorageProvider.cs
  27. 45
      src/Avalonia.Base/Platform/Storage/StorageProviderExtensions.cs
  28. 7
      src/Avalonia.Base/PropertyStore/EffectiveValue`1.cs
  29. 18
      src/Avalonia.Base/PropertyStore/ValueStore.cs
  30. 5
      src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual.cs
  31. 8
      src/Avalonia.Base/StyledElement.cs
  32. 2
      src/Avalonia.Base/StyledProperty.cs
  33. 24
      src/Avalonia.Base/Utilities/WeakEvents.cs
  34. 2
      src/Avalonia.Base/Visual.cs
  35. 1
      src/Avalonia.Base/composition-schema.xml
  36. 22
      src/Avalonia.Controls/Automation/Peers/SliderAutomationPeer.cs
  37. 6
      src/Avalonia.Controls/Platform/Dialogs/SystemDialogImpl.cs
  38. 5
      src/Avalonia.Controls/Primitives/AdornerLayer.cs
  39. 10
      src/Avalonia.Controls/Primitives/SelectingItemsControl.cs
  40. 39
      src/Avalonia.Controls/RelativePanel.AttachedProperties.cs
  41. 6
      src/Avalonia.Controls/Slider.cs
  42. 2
      src/Avalonia.Diagnostics/Diagnostics/Controls/FilterTextBox.axaml
  43. 8
      src/Avalonia.Diagnostics/Diagnostics/Screenshots/FilePickerHandler.cs
  44. 4
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/ControlLayoutViewModel.cs
  45. 2
      src/Avalonia.Dialogs/Internal/ManagedFileChooserViewModel.cs
  46. 2
      src/Avalonia.Dialogs/ManagedFileDialogExtensions.cs
  47. 2
      src/Avalonia.FreeDesktop/DBusSystemDialog.cs
  48. 2
      src/Avalonia.Headless/AvaloniaHeadlessPlatform.cs
  49. 6
      src/Avalonia.Native/SystemDialogs.cs
  50. 1
      src/Avalonia.OpenGL/Avalonia.OpenGL.csproj
  51. 5
      src/Avalonia.OpenGL/Controls/CompositionOpenGlSwapchain.cs
  52. 33
      src/Avalonia.OpenGL/Controls/OpenGlControlBase.cs
  53. 6
      src/Avalonia.OpenGL/Controls/OpenGlControlResources.cs
  54. 22
      src/Avalonia.OpenGL/Egl/EglContext.cs
  55. 11
      src/Avalonia.OpenGL/Egl/EglDisplay.cs
  56. 18
      src/Avalonia.OpenGL/Egl/EglDisplayOptions.cs
  57. 11
      src/Avalonia.OpenGL/Egl/EglDisplayUtils.cs
  58. 8
      src/Avalonia.OpenGL/Egl/EglGlPlatformSurface.cs
  59. 11
      src/Avalonia.OpenGL/Egl/EglGlPlatformSurfaceBase.cs
  60. 10
      src/Avalonia.OpenGL/Egl/EglInterface.cs
  61. 5
      src/Avalonia.OpenGL/Egl/EglPlatformGraphics.cs
  62. 43
      src/Avalonia.OpenGL/Features/ExternalObjectsOpenGlExtensionFeature.cs
  63. 16
      src/Avalonia.OpenGL/GlBasicInfoInterface.cs
  64. 21
      src/Avalonia.OpenGL/GlInterface.cs
  65. 3
      src/Avalonia.OpenGL/IGlContext.cs
  66. 3
      src/Avalonia.OpenGL/IOpenGlTextureSharingRenderInterfaceContextFeature.cs
  67. 6
      src/Avalonia.OpenGL/OpenGlException.cs
  68. 12
      src/Avalonia.X11/NativeDialogs/CompositeStorageProvider.cs
  69. 2
      src/Avalonia.X11/NativeDialogs/GtkNativeFileDialogs.cs
  70. 14
      src/Browser/Avalonia.Browser.Blazor/AvaloniaView.cs
  71. 37
      src/Browser/Avalonia.Browser.Blazor/BlazorSingleViewLifetime.cs
  72. 10
      src/Browser/Avalonia.Browser/AvaloniaView.cs
  73. 83
      src/Browser/Avalonia.Browser/BrowserAppBuilder.cs
  74. 40
      src/Browser/Avalonia.Browser/BrowserPlatformSettings.cs
  75. 51
      src/Browser/Avalonia.Browser/BrowserSingleViewLifetime.cs
  76. 4
      src/Browser/Avalonia.Browser/Interop/AvaloniaModule.cs
  77. 12
      src/Browser/Avalonia.Browser/Interop/StorageHelper.cs
  78. 28
      src/Browser/Avalonia.Browser/Storage/BrowserStorageProvider.cs
  79. 2
      src/Browser/Avalonia.Browser/webapp/.eslintrc.json
  80. 2
      src/Browser/Avalonia.Browser/webapp/build.js
  81. 18
      src/Browser/Avalonia.Browser/webapp/modules/avalonia.ts
  82. 12
      src/Browser/Avalonia.Browser/webapp/modules/avalonia/caniuse.ts
  83. 3
      src/Browser/Avalonia.Browser/webapp/modules/avalonia/stream.ts
  84. 50
      src/Browser/Avalonia.Browser/webapp/modules/storage/storageItem.ts
  85. 29
      src/Browser/Avalonia.Browser/webapp/modules/storage/storageProvider.ts
  86. 117
      src/Browser/Avalonia.Browser/webapp/package-lock.json
  87. 4
      src/Browser/Avalonia.Browser/webapp/package.json
  88. 6
      src/Browser/Avalonia.Browser/webapp/tsconfig.json
  89. 4
      src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindings/PropertyInfoAccessorFactory.cs
  90. 75
      src/Skia/Avalonia.Skia/DrawingContextImpl.cs
  91. 4
      src/Skia/Avalonia.Skia/GeometryImpl.cs
  92. 4
      src/Skia/Avalonia.Skia/Gpu/OpenGl/GlRenderTarget.cs
  93. 47
      src/Skia/Avalonia.Skia/PlatformRenderInterface.cs
  94. 72
      src/Skia/Avalonia.Skia/SKCacheBase.cs
  95. 13
      src/Skia/Avalonia.Skia/SKFontCache.cs
  96. 58
      src/Skia/Avalonia.Skia/SKPaintCache.cs
  97. 99
      src/Skia/Avalonia.Skia/SKRoundRectCache.cs
  98. 13
      src/Skia/Avalonia.Skia/SKTextBlobBuilderCache.cs
  99. 6
      src/Skia/Avalonia.Skia/TextShaperImpl.cs
  100. 19
      src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs

17
native/Avalonia.Native/src/OSX/AvnWindow.mm

@ -223,6 +223,19 @@
} }
} }
// From chromium:
//
// > The delegate or the window class should implement this method so that
// > -[NSWindow isZoomed] can be then determined by whether or not the current
// > window frame is equal to the zoomed frame.
//
// If we don't implement this, then isZoomed always returns true for a non-
// resizable window ¯\_(ツ)_/¯
- (NSRect)windowWillUseStandardFrame:(NSWindow*)window
defaultFrame:(NSRect)newFrame {
return newFrame;
}
-(BOOL)canBecomeKeyWindow -(BOOL)canBecomeKeyWindow
{ {
if(_canBecomeKeyWindow) if(_canBecomeKeyWindow)
@ -261,10 +274,6 @@
-(void) setEnabled:(bool)enable -(void) setEnabled:(bool)enable
{ {
_isEnabled = enable; _isEnabled = enable;
[[self standardWindowButton:NSWindowCloseButton] setEnabled:enable];
[[self standardWindowButton:NSWindowMiniaturizeButton] setEnabled:enable];
[[self standardWindowButton:NSWindowZoomButton] setEnabled:enable];
} }
-(void)becomeKeyWindow -(void)becomeKeyWindow

2
native/Avalonia.Native/src/OSX/PopupImpl.mm

@ -29,7 +29,7 @@ private:
[Window setLevel:NSPopUpMenuWindowLevel]; [Window setLevel:NSPopUpMenuWindowLevel];
} }
protected: protected:
virtual NSWindowStyleMask GetStyle() override virtual NSWindowStyleMask CalculateStyleMask() override
{ {
return NSWindowStyleMaskBorderless; return NSWindowStyleMaskBorderless;
} }

5
native/Avalonia.Native/src/OSX/WindowBaseImpl.h

@ -105,9 +105,8 @@ BEGIN_INTERFACE_MAP()
virtual void BringToFront (); virtual void BringToFront ();
protected: protected:
virtual NSWindowStyleMask GetStyle(); virtual NSWindowStyleMask CalculateStyleMask() = 0;
virtual void UpdateStyle();
void UpdateStyle();
private: private:
void CreateNSWindow (bool isDialog); void CreateNSWindow (bool isDialog);

29
native/Avalonia.Native/src/OSX/WindowBaseImpl.mm

@ -35,18 +35,14 @@ WindowBaseImpl::WindowBaseImpl(IAvnWindowBaseEvents *events, IAvnGlContext *gl,
lastSize = NSSize { 100, 100 }; lastSize = NSSize { 100, 100 };
lastMaxSize = NSSize { CGFLOAT_MAX, CGFLOAT_MAX}; lastMaxSize = NSSize { CGFLOAT_MAX, CGFLOAT_MAX};
lastMinSize = NSSize { 0, 0 }; lastMinSize = NSSize { 0, 0 };
lastMenu = nullptr; lastMenu = nullptr;
CreateNSWindow(usePanel); CreateNSWindow(usePanel);
[Window setContentView:StandardContainer]; [Window setContentView:StandardContainer];
[Window setStyleMask:NSWindowStyleMaskBorderless];
[Window setBackingType:NSBackingStoreBuffered]; [Window setBackingType:NSBackingStoreBuffered];
[Window setContentMinSize:lastMinSize]; [Window setContentMinSize:lastMinSize];
[Window setContentMaxSize:lastMaxSize]; [Window setContentMaxSize:lastMaxSize];
[Window setOpaque:false]; [Window setOpaque:false];
} }
@ -564,12 +560,8 @@ bool WindowBaseImpl::IsModal() {
return false; return false;
} }
NSWindowStyleMask WindowBaseImpl::GetStyle() {
return NSWindowStyleMaskBorderless;
}
void WindowBaseImpl::UpdateStyle() { void WindowBaseImpl::UpdateStyle() {
[Window setStyleMask:GetStyle()]; [Window setStyleMask:CalculateStyleMask()];
} }
void WindowBaseImpl::CleanNSWindow() { void WindowBaseImpl::CleanNSWindow() {
@ -580,21 +572,12 @@ void WindowBaseImpl::CleanNSWindow() {
} }
} }
void WindowBaseImpl::CreateNSWindow(bool isDialog) { void WindowBaseImpl::CreateNSWindow(bool usePanel) {
if (isDialog) { if (usePanel) {
if (![Window isKindOfClass:[AvnPanel class]]) { Window = [[AvnPanel alloc] initWithParent:this contentRect:NSRect{0, 0, lastSize} styleMask:NSWindowStyleMaskBorderless];
CleanNSWindow(); [Window setHidesOnDeactivate:false];
Window = [[AvnPanel alloc] initWithParent:this contentRect:NSRect{0, 0, lastSize} styleMask:GetStyle()];
[Window setHidesOnDeactivate:false];
}
} else { } else {
if (![Window isKindOfClass:[AvnWindow class]]) { Window = [[AvnWindow alloc] initWithParent:this contentRect:NSRect{0, 0, lastSize} styleMask:NSWindowStyleMaskBorderless];
CleanNSWindow();
Window = [[AvnWindow alloc] initWithParent:this contentRect:NSRect{0, 0, lastSize} styleMask:GetStyle()];
}
} }
} }

6
native/Avalonia.Native/src/OSX/WindowImpl.h

@ -41,8 +41,6 @@ BEGIN_INTERFACE_MAP()
WindowImpl(IAvnWindowEvents* events, IAvnGlContext* gl); WindowImpl(IAvnWindowEvents* events, IAvnGlContext* gl);
void HideOrShowTrafficLights ();
virtual HRESULT Show (bool activate, bool isDialog) override; virtual HRESULT Show (bool activate, bool isDialog) override;
virtual HRESULT SetEnabled (bool enable) override; virtual HRESULT SetEnabled (bool enable) override;
@ -100,9 +98,11 @@ BEGIN_INTERFACE_MAP()
bool CanBecomeKeyWindow (); bool CanBecomeKeyWindow ();
protected: protected:
virtual NSWindowStyleMask GetStyle() override; virtual NSWindowStyleMask CalculateStyleMask() override;
void UpdateStyle () override;
private: private:
void ZOrderChildWindows();
void OnInitialiseNSWindow(); void OnInitialiseNSWindow();
NSString *_lastTitle; NSString *_lastTitle;
}; };

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

@ -30,19 +30,6 @@ WindowImpl::WindowImpl(IAvnWindowEvents *events, IAvnGlContext *gl) : WindowBase
OnInitialiseNSWindow(); OnInitialiseNSWindow();
} }
void WindowImpl::HideOrShowTrafficLights() {
if (Window == nil) {
return;
}
bool wantsChrome = (_extendClientHints & AvnSystemChrome) || (_extendClientHints & AvnPreferSystemChrome);
bool hasTrafficLights = _isClientAreaExtended ? wantsChrome : _decorations == SystemDecorationsFull;
[[Window standardWindowButton:NSWindowCloseButton] setHidden:!hasTrafficLights];
[[Window standardWindowButton:NSWindowMiniaturizeButton] setHidden:!hasTrafficLights];
[[Window standardWindowButton:NSWindowZoomButton] setHidden:!hasTrafficLights];
}
void WindowImpl::OnInitialiseNSWindow(){ void WindowImpl::OnInitialiseNSWindow(){
[GetWindowProtocol() setCanBecomeKeyWindow:true]; [GetWindowProtocol() setCanBecomeKeyWindow:true];
@ -67,8 +54,6 @@ HRESULT WindowImpl::Show(bool activate, bool isDialog) {
WindowBaseImpl::Show(activate, isDialog); WindowBaseImpl::Show(activate, isDialog);
GetWindowState(&_actualWindowState); GetWindowState(&_actualWindowState);
HideOrShowTrafficLights();
return SetWindowState(_lastWindowState); return SetWindowState(_lastWindowState);
} }
} }
@ -134,14 +119,19 @@ void WindowImpl::BringToFront()
} }
[Window invalidateShadow]; [Window invalidateShadow];
ZOrderChildWindows();
}
}
void WindowImpl::ZOrderChildWindows()
{
for(auto iterator = _children.begin(); iterator != _children.end(); iterator++)
{
auto window = (*iterator)->Window;
for(auto iterator = _children.begin(); iterator != _children.end(); iterator++) // #9565: Only bring window to front if it's on the currently active space
{ if ([window isOnActiveSpace]) {
auto window = (*iterator)->Window; (*iterator)->BringToFront();
// #9565: Only bring window to front if it's on the currently active space
if ([window isOnActiveSpace])
(*iterator)->BringToFront();
} }
} }
} }
@ -161,13 +151,15 @@ bool WindowImpl::CanBecomeKeyWindow()
void WindowImpl::StartStateTransition() { void WindowImpl::StartStateTransition() {
_transitioningWindowState = true; _transitioningWindowState = true;
UpdateStyle();
} }
void WindowImpl::EndStateTransition() { void WindowImpl::EndStateTransition() {
_transitioningWindowState = false; _transitioningWindowState = false;
UpdateStyle();
// Ensure correct order of child windows after fullscreen transition. // Ensure correct order of child windows after fullscreen transition.
BringToFront(); ZOrderChildWindows();
} }
SystemDecorations WindowImpl::Decorations() { SystemDecorations WindowImpl::Decorations() {
@ -225,16 +217,12 @@ bool WindowImpl::IsZoomed() {
} }
void WindowImpl::DoZoom() { void WindowImpl::DoZoom() {
switch (_decorations) { if (_decorations == SystemDecorationsNone ||
case SystemDecorationsNone: _decorations == SystemDecorationsBorderOnly ||
case SystemDecorationsBorderOnly: _canResize == false) {
[Window setFrame:[Window screen].visibleFrame display:true]; [Window setFrame:[Window screen].visibleFrame display:true];
break; } else {
[Window performZoom:Window];
case SystemDecorationsFull:
[Window performZoom:Window];
break;
} }
} }
@ -261,8 +249,6 @@ HRESULT WindowImpl::SetDecorations(SystemDecorations value) {
UpdateStyle(); UpdateStyle();
HideOrShowTrafficLights();
switch (_decorations) { switch (_decorations) {
case SystemDecorationsNone: case SystemDecorationsNone:
[Window setHasShadow:NO]; [Window setHasShadow:NO];
@ -419,9 +405,6 @@ HRESULT WindowImpl::SetExtendClientArea(bool enable) {
} }
[GetWindowProtocol() setIsExtended:enable]; [GetWindowProtocol() setIsExtended:enable];
HideOrShowTrafficLights();
UpdateStyle(); UpdateStyle();
} }
@ -577,14 +560,16 @@ bool WindowImpl::IsOwned() {
return _parent != nullptr; return _parent != nullptr;
} }
NSWindowStyleMask WindowImpl::GetStyle() { NSWindowStyleMask WindowImpl::CalculateStyleMask() {
unsigned long s = NSWindowStyleMaskBorderless; // Use the current style mask and only clear the flags we're going to be modifying.
unsigned long s = [Window styleMask] &
~(NSWindowStyleMaskFullSizeContentView |
NSWindowStyleMaskTitled |
NSWindowStyleMaskClosable |
NSWindowStyleMaskResizable |
NSWindowStyleMaskMiniaturizable |
NSWindowStyleMaskTexturedBackground);
if(_actualWindowState == FullScreen)
{
s |= NSWindowStyleMaskFullScreen;
}
switch (_decorations) { switch (_decorations) {
case SystemDecorationsNone: case SystemDecorationsNone:
s = s | NSWindowStyleMaskFullSizeContentView; s = s | NSWindowStyleMaskFullSizeContentView;
@ -597,7 +582,7 @@ NSWindowStyleMask WindowImpl::GetStyle() {
case SystemDecorationsFull: case SystemDecorationsFull:
s = s | NSWindowStyleMaskTitled | NSWindowStyleMaskClosable; s = s | NSWindowStyleMaskTitled | NSWindowStyleMaskClosable;
if (_canResize && _isEnabled) { if ((_canResize && _isEnabled) || _transitioningWindowState) {
s = s | NSWindowStyleMaskResizable; s = s | NSWindowStyleMaskResizable;
} }
break; break;
@ -612,3 +597,25 @@ NSWindowStyleMask WindowImpl::GetStyle() {
} }
return s; return s;
} }
void WindowImpl::UpdateStyle() {
WindowBaseImpl::UpdateStyle();
if (Window == nil) {
return;
}
bool wantsChrome = (_extendClientHints & AvnSystemChrome) || (_extendClientHints & AvnPreferSystemChrome);
bool hasTrafficLights = _isClientAreaExtended ? wantsChrome : _decorations == SystemDecorationsFull;
NSButton* closeButton = [Window standardWindowButton:NSWindowCloseButton];
NSButton* miniaturizeButton = [Window standardWindowButton:NSWindowMiniaturizeButton];
NSButton* zoomButton = [Window standardWindowButton:NSWindowZoomButton];
[closeButton setHidden:!hasTrafficLights];
[closeButton setEnabled:_isEnabled];
[miniaturizeButton setHidden:!hasTrafficLights];
[miniaturizeButton setEnabled:_isEnabled];
[zoomButton setHidden:!hasTrafficLights];
[zoomButton setEnabled:_isEnabled && _canResize];
}

11
samples/ControlCatalog.Browser.Blazor/App.razor.cs

@ -1,3 +1,5 @@
using System;
using System.Threading.Tasks;
using Avalonia; using Avalonia;
using Avalonia.Browser.Blazor; using Avalonia.Browser.Blazor;
@ -5,13 +7,4 @@ namespace ControlCatalog.Browser.Blazor;
public partial class App public partial class App
{ {
protected override void OnParametersSet()
{
AppBuilder.Configure<ControlCatalog.App>()
.UseBlazor()
// .With(new SkiaOptions { CustomGpuFactory = null }) // uncomment to disable GPU/GL rendering
.SetupWithSingleViewLifetime();
base.OnParametersSet();
}
} }

12
samples/ControlCatalog.Browser.Blazor/Program.cs

@ -1,6 +1,8 @@
using System; using System;
using System.Net.Http; using System.Net.Http;
using System.Threading.Tasks; using System.Threading.Tasks;
using Avalonia;
using Avalonia.Browser.Blazor;
using Microsoft.AspNetCore.Components.WebAssembly.Hosting; using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using ControlCatalog.Browser.Blazor; using ControlCatalog.Browser.Blazor;
@ -9,9 +11,17 @@ public class Program
{ {
public static async Task Main(string[] args) public static async Task Main(string[] args)
{ {
await CreateHostBuilder(args).Build().RunAsync(); var host = CreateHostBuilder(args).Build();
await StartAvaloniaApp();
await host.RunAsync();
} }
public static async Task StartAvaloniaApp()
{
await AppBuilder.Configure<ControlCatalog.App>()
.StartBlazorAppAsync();
}
public static WebAssemblyHostBuilder CreateHostBuilder(string[] args) public static WebAssemblyHostBuilder CreateHostBuilder(string[] args)
{ {
var builder = WebAssemblyHostBuilder.CreateDefault(args); var builder = WebAssemblyHostBuilder.CreateDefault(args);

20
samples/ControlCatalog.Browser/Program.cs

@ -1,6 +1,8 @@
using System.Runtime.Versioning; using System.Runtime.Versioning;
using System.Threading.Tasks;
using Avalonia; using Avalonia;
using Avalonia.Browser; using Avalonia.Browser;
using Avalonia.Controls;
using ControlCatalog; using ControlCatalog;
using ControlCatalog.Browser; using ControlCatalog.Browser;
@ -8,15 +10,27 @@ using ControlCatalog.Browser;
internal partial class Program internal partial class Program
{ {
private static void Main(string[] args) public static async Task Main(string[] args)
{ {
BuildAvaloniaApp() await BuildAvaloniaApp()
.AfterSetup(_ => .AfterSetup(_ =>
{ {
ControlCatalog.Pages.EmbedSample.Implementation = new EmbedSampleWeb(); ControlCatalog.Pages.EmbedSample.Implementation = new EmbedSampleWeb();
}).SetupBrowserApp("out"); })
.StartBrowserAppAsync("out");
} }
// Example without a ISingleViewApplicationLifetime
// private static AvaloniaView _avaloniaView;
// public static async Task Main(string[] args)
// {
// await BuildAvaloniaApp()
// .SetupBrowserApp();
//
// _avaloniaView = new AvaloniaView("out");
// _avaloniaView.Content = new TextBlock { Text = "Hello world" };
// }
public static AppBuilder BuildAvaloniaApp() public static AppBuilder BuildAvaloniaApp()
=> AppBuilder.Configure<App>(); => AppBuilder.Configure<App>();
} }

3
samples/ControlCatalog.Browser/main.js

@ -2,7 +2,6 @@
// The .NET Foundation licenses this file to you under the MIT license. // The .NET Foundation licenses this file to you under the MIT license.
import { dotnet } from './dotnet.js' import { dotnet } from './dotnet.js'
import { registerAvaloniaModule } from './avalonia.js';
const is_browser = typeof window != "undefined"; const is_browser = typeof window != "undefined";
if (!is_browser) throw new Error(`Expected to be running in a browser`); if (!is_browser) throw new Error(`Expected to be running in a browser`);
@ -12,8 +11,6 @@ const dotnetRuntime = await dotnet
.withApplicationArgumentsFromQuery() .withApplicationArgumentsFromQuery()
.create(); .create();
await registerAvaloniaModule(dotnetRuntime);
const config = dotnetRuntime.getConfig(); const config = dotnetRuntime.getConfig();
await dotnetRuntime.runMainAndExit(config.mainAssemblyName, ["dotnet", "is", "great!"]); await dotnetRuntime.runMainAndExit(config.mainAssemblyName, ["dotnet", "is", "great!"]);

51
samples/ControlCatalog/Pages/DialogsPage.xaml.cs

@ -40,7 +40,7 @@ namespace ControlCatalog.Pages
if (Enum.TryParse<WellKnownFolder>(currentFolderBox.Text, true, out var folderEnum)) if (Enum.TryParse<WellKnownFolder>(currentFolderBox.Text, true, out var folderEnum))
{ {
lastSelectedDirectory = await GetStorageProvider().TryGetWellKnownFolder(folderEnum); lastSelectedDirectory = await GetStorageProvider().TryGetWellKnownFolderAsync(folderEnum);
} }
else else
{ {
@ -51,7 +51,7 @@ namespace ControlCatalog.Pages
if (folderLink is not null) if (folderLink is not null)
{ {
lastSelectedDirectory = await GetStorageProvider().TryGetFolderFromPath(folderLink); lastSelectedDirectory = await GetStorageProvider().TryGetFolderFromPathAsync(folderLink);
} }
} }
}; };
@ -82,7 +82,13 @@ namespace ControlCatalog.Pages
return new List<FilePickerFileType> return new List<FilePickerFileType>
{ {
FilePickerFileTypes.All, FilePickerFileTypes.All,
FilePickerFileTypes.TextPlain FilePickerFileTypes.TextPlain,
new("Binary Log")
{
Patterns = new[] { "*.binlog", "*.buildlog" },
MimeTypes = new[] { "application/binlog", "application/buildlog" },
AppleUniformTypeIdentifiers = new []{ "public.data" }
}
}; };
} }
@ -142,7 +148,7 @@ namespace ControlCatalog.Pages
} }
else else
{ {
SetFolder(await GetStorageProvider().TryGetFolderFromPath(result)); SetFolder(await GetStorageProvider().TryGetFolderFromPathAsync(result));
results.Items = new[] { result }; results.Items = new[] { result };
resultsVisible.IsVisible = true; resultsVisible.IsVisible = true;
} }
@ -223,7 +229,7 @@ namespace ControlCatalog.Pages
ShowOverwritePrompt = false ShowOverwritePrompt = false
}); });
if (file is not null && file.CanOpenWrite) if (file is not null)
{ {
// Sync disposal of StreamWriter is not supported on WASM // Sync disposal of StreamWriter is not supported on WASM
#if NET6_0_OR_GREATER #if NET6_0_OR_GREATER
@ -275,7 +281,7 @@ namespace ControlCatalog.Pages
{ {
ignoreTextChanged = true; ignoreTextChanged = true;
lastSelectedDirectory = folder; lastSelectedDirectory = folder;
currentFolderBox.Text = folder?.Path.LocalPath; currentFolderBox.Text = folder?.Path is { IsAbsoluteUri: true } abs ? abs.LocalPath : folder?.Path?.ToString();
ignoreTextChanged = false; ignoreTextChanged = false;
} }
async Task SetPickerResult(IReadOnlyCollection<IStorageItem>? items) async Task SetPickerResult(IReadOnlyCollection<IStorageItem>? items)
@ -298,31 +304,26 @@ namespace ControlCatalog.Pages
if (item is IStorageFile file) if (item is IStorageFile file)
{ {
resultText += @$" resultText += @$"
CanOpenRead: {file.CanOpenRead}
CanOpenWrite: {file.CanOpenWrite}
Content: Content:
"; ";
if (file.CanOpenRead)
{
#if NET6_0_OR_GREATER #if NET6_0_OR_GREATER
await using var stream = await file.OpenReadAsync(); await using var stream = await file.OpenReadAsync();
#else #else
using var stream = await file.OpenReadAsync(); using var stream = await file.OpenReadAsync();
#endif #endif
using var reader = new System.IO.StreamReader(stream); using var reader = new System.IO.StreamReader(stream);
// 4GB file test, shouldn't load more than 10000 chars into a memory. // 4GB file test, shouldn't load more than 10000 chars into a memory.
const int length = 10000; const int length = 10000;
var buffer = ArrayPool<char>.Shared.Rent(length); var buffer = ArrayPool<char>.Shared.Rent(length);
try try
{ {
var charsRead = await reader.ReadAsync(buffer, 0, length); var charsRead = await reader.ReadAsync(buffer, 0, length);
resultText += new string(buffer, 0, charsRead); resultText += new string(buffer, 0, charsRead);
} }
finally finally
{ {
ArrayPool<char>.Shared.Return(buffer); ArrayPool<char>.Shared.Return(buffer);
}
} }
} }

4
samples/IntegrationTestApp/MainWindow.axaml

@ -140,6 +140,7 @@
<ComboBoxItem Name="ShowWindowStateMaximized">Maximized</ComboBoxItem> <ComboBoxItem Name="ShowWindowStateMaximized">Maximized</ComboBoxItem>
<ComboBoxItem Name="ShowWindowStateFullScreen">FullScreen</ComboBoxItem> <ComboBoxItem Name="ShowWindowStateFullScreen">FullScreen</ComboBoxItem>
</ComboBox> </ComboBox>
<CheckBox Name="ShowWindowCanResize" IsChecked="True">Can Resize</CheckBox>
<Button Name="ShowWindow">Show Window</Button> <Button Name="ShowWindow">Show Window</Button>
<Button Name="SendToBack">Send to Back</Button> <Button Name="SendToBack">Send to Back</Button>
<Button Name="EnterFullscreen">Enter Fullscreen</Button> <Button Name="EnterFullscreen">Enter Fullscreen</Button>
@ -152,6 +153,9 @@
</StackPanel> </StackPanel>
</Grid> </Grid>
</TabItem> </TabItem>
<TabItem Header="SliderTab">
<Slider VerticalAlignment="Top" Name="Slider" Value="30"/>
</TabItem>
</TabControl> </TabControl>
</DockPanel> </DockPanel>
</Window> </Window>

2
samples/IntegrationTestApp/MainWindow.axaml.cs

@ -66,11 +66,13 @@ namespace IntegrationTestApp
var locationComboBox = this.GetControl<ComboBox>("ShowWindowLocation"); var locationComboBox = this.GetControl<ComboBox>("ShowWindowLocation");
var stateComboBox = this.GetControl<ComboBox>("ShowWindowState"); var stateComboBox = this.GetControl<ComboBox>("ShowWindowState");
var size = !string.IsNullOrWhiteSpace(sizeTextBox.Text) ? Size.Parse(sizeTextBox.Text) : (Size?)null; var size = !string.IsNullOrWhiteSpace(sizeTextBox.Text) ? Size.Parse(sizeTextBox.Text) : (Size?)null;
var canResizeCheckBox = this.GetControl<CheckBox>("ShowWindowCanResize");
var owner = (Window)this.GetVisualRoot()!; var owner = (Window)this.GetVisualRoot()!;
var window = new ShowWindowTest var window = new ShowWindowTest
{ {
WindowStartupLocation = (WindowStartupLocation)locationComboBox.SelectedIndex, WindowStartupLocation = (WindowStartupLocation)locationComboBox.SelectedIndex,
CanResize = canResizeCheckBox.IsChecked.Value,
}; };
if (Application.Current?.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime lifetime) if (Application.Current?.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime lifetime)

6
src/Android/Avalonia.Android/Platform/Storage/AndroidStorageItem.cs

@ -177,11 +177,7 @@ internal sealed class AndroidStorageFile : AndroidStorageItem, IStorageBookmarkF
public AndroidStorageFile(Activity activity, AndroidUri uri) : base(activity, uri, false) public AndroidStorageFile(Activity activity, AndroidUri uri) : base(activity, uri, false)
{ {
} }
public bool CanOpenRead => true;
public bool CanOpenWrite => true;
public Task<Stream> OpenReadAsync() => Task.FromResult(OpenContentStream(Activity, Uri, false) public Task<Stream> OpenReadAsync() => Task.FromResult(OpenContentStream(Activity, Uri, false)
?? throw new InvalidOperationException("Failed to open content stream")); ?? throw new InvalidOperationException("Failed to open content stream"));

6
src/Android/Avalonia.Android/Platform/Storage/AndroidStorageProvider.cs

@ -37,7 +37,7 @@ internal class AndroidStorageProvider : IStorageProvider
return Task.FromResult<IStorageBookmarkFolder?>(new AndroidStorageFolder(_activity, uri, false)); return Task.FromResult<IStorageBookmarkFolder?>(new AndroidStorageFolder(_activity, uri, false));
} }
public async Task<IStorageFile?> TryGetFileFromPath(Uri filePath) public async Task<IStorageFile?> TryGetFileFromPathAsync(Uri filePath)
{ {
if (filePath is null) if (filePath is null)
{ {
@ -70,7 +70,7 @@ internal class AndroidStorageProvider : IStorageProvider
return new AndroidStorageFile(_activity, androidUri); return new AndroidStorageFile(_activity, androidUri);
} }
public async Task<IStorageFolder?> TryGetFolderFromPath(Uri folderPath) public async Task<IStorageFolder?> TryGetFolderFromPathAsync(Uri folderPath)
{ {
if (folderPath is null) if (folderPath is null)
{ {
@ -103,7 +103,7 @@ internal class AndroidStorageProvider : IStorageProvider
return new AndroidStorageFolder(_activity, androidUri, false); return new AndroidStorageFolder(_activity, androidUri, false);
} }
public Task<IStorageFolder?> TryGetWellKnownFolder(WellKnownFolder wellKnownFolder) public Task<IStorageFolder?> TryGetWellKnownFolderAsync(WellKnownFolder wellKnownFolder)
{ {
var dirCode = wellKnownFolder switch var dirCode = wellKnownFolder switch
{ {

10
src/Avalonia.Base/AvaloniaObject.cs

@ -664,14 +664,12 @@ namespace Avalonia
/// <param name="property">The property that has changed.</param> /// <param name="property">The property that has changed.</param>
/// <param name="oldValue">The old property value.</param> /// <param name="oldValue">The old property value.</param>
/// <param name="newValue">The new property value.</param> /// <param name="newValue">The new property value.</param>
/// <param name="priority">The priority of the binding that produced the value.</param>
protected void RaisePropertyChanged<T>( protected void RaisePropertyChanged<T>(
DirectPropertyBase<T> property, DirectPropertyBase<T> property,
Optional<T> oldValue, T oldValue,
BindingValue<T> newValue, T newValue)
BindingPriority priority = BindingPriority.LocalValue)
{ {
RaisePropertyChanged(property, oldValue, newValue, priority, true); RaisePropertyChanged(property, oldValue, newValue, BindingPriority.LocalValue, true);
} }
/// <summary> /// <summary>
@ -720,7 +718,7 @@ namespace Avalonia
/// <returns> /// <returns>
/// True if the value changed, otherwise false. /// True if the value changed, otherwise false.
/// </returns> /// </returns>
protected bool SetAndRaise<T>(AvaloniaProperty<T> property, ref T field, T value) protected bool SetAndRaise<T>(DirectPropertyBase<T> property, ref T field, T value)
{ {
VerifyAccess(); VerifyAccess();

4
src/Avalonia.Base/Data/Core/IndexerNodeBase.cs

@ -22,7 +22,7 @@ namespace Avalonia.Data.Core
if (target is INotifyPropertyChanged inpc) if (target is INotifyPropertyChanged inpc)
{ {
WeakEvents.PropertyChanged.Subscribe(inpc, this); WeakEvents.ThreadSafePropertyChanged.Subscribe(inpc, this);
} }
ValueChanged(GetValue(target)); ValueChanged(GetValue(target));
@ -39,7 +39,7 @@ namespace Avalonia.Data.Core
if (target is INotifyPropertyChanged inpc) if (target is INotifyPropertyChanged inpc)
{ {
WeakEvents.PropertyChanged.Unsubscribe(inpc, this); WeakEvents.ThreadSafePropertyChanged.Unsubscribe(inpc, this);
} }
} }
} }

4
src/Avalonia.Base/Data/Core/Plugins/InpcPropertyAccessorPlugin.cs

@ -160,7 +160,7 @@ namespace Avalonia.Data.Core.Plugins
var inpc = GetReferenceTarget() as INotifyPropertyChanged; var inpc = GetReferenceTarget() as INotifyPropertyChanged;
if (inpc != null) if (inpc != null)
WeakEvents.PropertyChanged.Unsubscribe(inpc, this); WeakEvents.ThreadSafePropertyChanged.Unsubscribe(inpc, this);
} }
private object? GetReferenceTarget() private object? GetReferenceTarget()
@ -185,7 +185,7 @@ namespace Avalonia.Data.Core.Plugins
var inpc = GetReferenceTarget() as INotifyPropertyChanged; var inpc = GetReferenceTarget() as INotifyPropertyChanged;
if (inpc != null) if (inpc != null)
WeakEvents.PropertyChanged.Subscribe(inpc, this); WeakEvents.ThreadSafePropertyChanged.Subscribe(inpc, this);
} }
} }
} }

27
src/Avalonia.Base/Layout/LayoutInformation.cs

@ -0,0 +1,27 @@
namespace Avalonia.Layout;
/// <summary>
/// Provides access to layout information of a control.
/// </summary>
public static class LayoutInformation
{
/// <summary>
/// Gets the available size constraint passed in the previous layout pass.
/// </summary>
/// <param name="control">The control.</param>
/// <returns>Previous control measure constraint, if any.</returns>
public static Size? GetPreviousMeasureConstraint(Layoutable control)
{
return control.PreviousMeasure;
}
/// <summary>
/// Gets the control bounds used in the previous layout arrange pass.
/// </summary>
/// <param name="control">The control.</param>
/// <returns>Previous control arrange bounds, if any.</returns>
public static Rect? GetPreviousArrangeBounds(Layoutable control)
{
return control.PreviousArrange;
}
}

3
src/Avalonia.Base/Layout/Layoutable.cs

@ -323,6 +323,9 @@ namespace Avalonia.Layout
set { SetValue(UseLayoutRoundingProperty, value); } set { SetValue(UseLayoutRoundingProperty, value); }
} }
/// <summary>
/// Gets the available size passed in the previous layout pass, if any.
/// </summary>
internal Size? PreviousMeasure => _previousMeasure; internal Size? PreviousMeasure => _previousMeasure;
/// <summary> /// <summary>

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

@ -37,7 +37,7 @@ namespace Avalonia.Platform
}; };
} }
public event EventHandler<PlatformColorValues>? ColorValuesChanged; public virtual event EventHandler<PlatformColorValues>? ColorValuesChanged;
protected void OnColorValuesChanged(PlatformColorValues colorValues) protected void OnColorValuesChanged(PlatformColorValues colorValues)
{ {

6
src/Avalonia.Base/Platform/Storage/FileIO/BclStorageFile.cs

@ -18,11 +18,7 @@ internal class BclStorageFile : IStorageBookmarkFile
} }
public FileInfo FileInfo { get; } public FileInfo FileInfo { get; }
public bool CanOpenRead => true;
public bool CanOpenWrite => true;
public string Name => FileInfo.Name; public string Name => FileInfo.Name;
public virtual bool CanBookmark => true; public virtual bool CanBookmark => true;

6
src/Avalonia.Base/Platform/Storage/FileIO/BclStorageProvider.cs

@ -34,7 +34,7 @@ internal abstract class BclStorageProvider : IStorageProvider
: Task.FromResult<IStorageBookmarkFolder?>(null); : Task.FromResult<IStorageBookmarkFolder?>(null);
} }
public virtual Task<IStorageFile?> TryGetFileFromPath(Uri filePath) public virtual Task<IStorageFile?> TryGetFileFromPathAsync(Uri filePath)
{ {
if (filePath.IsAbsoluteUri) if (filePath.IsAbsoluteUri)
{ {
@ -48,7 +48,7 @@ internal abstract class BclStorageProvider : IStorageProvider
return Task.FromResult<IStorageFile?>(null); return Task.FromResult<IStorageFile?>(null);
} }
public virtual Task<IStorageFolder?> TryGetFolderFromPath(Uri folderPath) public virtual Task<IStorageFolder?> TryGetFolderFromPathAsync(Uri folderPath)
{ {
if (folderPath.IsAbsoluteUri) if (folderPath.IsAbsoluteUri)
{ {
@ -62,7 +62,7 @@ internal abstract class BclStorageProvider : IStorageProvider
return Task.FromResult<IStorageFolder?>(null); return Task.FromResult<IStorageFolder?>(null);
} }
public virtual Task<IStorageFolder?> TryGetWellKnownFolder(WellKnownFolder wellKnownFolder) public virtual Task<IStorageFolder?> TryGetWellKnownFolderAsync(WellKnownFolder wellKnownFolder)
{ {
// Note, this BCL API returns different values depending on the .NET version. // Note, this BCL API returns different values depending on the .NET version.
// We should also document it. // We should also document it.

16
src/Avalonia.Base/Platform/Storage/FilePickerFileType.cs

@ -1,4 +1,6 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.IO;
using System.Linq;
namespace Avalonia.Platform.Storage; namespace Avalonia.Platform.Storage;
@ -21,7 +23,7 @@ public sealed class FilePickerFileType
/// List of extensions in GLOB format. I.e. "*.png" or "*.*". /// List of extensions in GLOB format. I.e. "*.png" or "*.*".
/// </summary> /// </summary>
/// <remarks> /// <remarks>
/// Used on Windows and Linux systems. /// Used on Windows, Linux and Browser platforms.
/// </remarks> /// </remarks>
public IReadOnlyList<string>? Patterns { get; set; } public IReadOnlyList<string>? Patterns { get; set; }
@ -29,7 +31,7 @@ public sealed class FilePickerFileType
/// List of extensions in MIME format. /// List of extensions in MIME format.
/// </summary> /// </summary>
/// <remarks> /// <remarks>
/// Used on Android, Browser and Linux systems. /// Used on Android, Linux and Browser platforms.
/// </remarks> /// </remarks>
public IReadOnlyList<string>? MimeTypes { get; set; } public IReadOnlyList<string>? MimeTypes { get; set; }
@ -41,4 +43,14 @@ public sealed class FilePickerFileType
/// See https://developer.apple.com/documentation/uniformtypeidentifiers/system_declared_uniform_type_identifiers. /// See https://developer.apple.com/documentation/uniformtypeidentifiers/system_declared_uniform_type_identifiers.
/// </remarks> /// </remarks>
public IReadOnlyList<string>? AppleUniformTypeIdentifiers { get; set; } public IReadOnlyList<string>? AppleUniformTypeIdentifiers { get; set; }
internal IReadOnlyList<string>? TryGetExtensions()
{
// Converts random glob pattern to a simple extension name.
// GetExtension should be sufficient here.
// Only exception is "*.*proj" patterns that should be filtered as well.
return Patterns?.Select(Path.GetExtension)
.Where(e => !string.IsNullOrEmpty(e) && !e.Contains('*') && e.StartsWith("."))
.ToArray()!;
}
} }

12
src/Avalonia.Base/Platform/Storage/IStorageFile.cs

@ -10,22 +10,12 @@ namespace Avalonia.Platform.Storage;
[NotClientImplementable] [NotClientImplementable]
public interface IStorageFile : IStorageItem public interface IStorageFile : IStorageItem
{ {
/// <summary>
/// Returns true, if file is readable.
/// </summary>
bool CanOpenRead { get; }
/// <summary> /// <summary>
/// Opens a stream for read access. /// Opens a stream for read access.
/// </summary> /// </summary>
/// <exception cref="System.UnauthorizedAccessException" /> /// <exception cref="System.UnauthorizedAccessException" />
Task<Stream> OpenReadAsync(); Task<Stream> OpenReadAsync();
/// <summary>
/// Returns true, if file is writeable.
/// </summary>
bool CanOpenWrite { get; }
/// <summary> /// <summary>
/// Opens stream for writing to the file. /// Opens stream for writing to the file.
/// </summary> /// </summary>

6
src/Avalonia.Base/Platform/Storage/IStorageProvider.cs

@ -66,7 +66,7 @@ public interface IStorageProvider
/// It also might ask user for the permission, and throw an exception if it was denied. /// It also might ask user for the permission, and throw an exception if it was denied.
/// </remarks> /// </remarks>
/// <returns>File or null if it doesn't exist.</returns> /// <returns>File or null if it doesn't exist.</returns>
Task<IStorageFile?> TryGetFileFromPath(Uri filePath); Task<IStorageFile?> TryGetFileFromPathAsync(Uri filePath);
/// <summary> /// <summary>
/// Attempts to read folder from the file-system by its path. /// Attempts to read folder from the file-system by its path.
@ -78,12 +78,12 @@ public interface IStorageProvider
/// It also might ask user for the permission, and throw an exception if it was denied. /// It also might ask user for the permission, and throw an exception if it was denied.
/// </remarks> /// </remarks>
/// <returns>Folder or null if it doesn't exist.</returns> /// <returns>Folder or null if it doesn't exist.</returns>
Task<IStorageFolder?> TryGetFolderFromPath(Uri folderPath); Task<IStorageFolder?> TryGetFolderFromPathAsync(Uri folderPath);
/// <summary> /// <summary>
/// Attempts to read folder from the file-system by its path /// Attempts to read folder from the file-system by its path
/// </summary> /// </summary>
/// <param name="wellKnownFolder">Well known folder identifier.</param> /// <param name="wellKnownFolder">Well known folder identifier.</param>
/// <returns>Folder or null if it doesn't exist.</returns> /// <returns>Folder or null if it doesn't exist.</returns>
Task<IStorageFolder?> TryGetWellKnownFolder(WellKnownFolder wellKnownFolder); Task<IStorageFolder?> TryGetWellKnownFolderAsync(WellKnownFolder wellKnownFolder);
} }

45
src/Avalonia.Base/Platform/Storage/StorageProviderExtensions.cs

@ -8,48 +8,47 @@ namespace Avalonia.Platform.Storage;
/// </summary> /// </summary>
public static class StorageProviderExtensions public static class StorageProviderExtensions
{ {
/// <inheritdoc cref="IStorageProvider.TryGetFileFromPath"/> /// <inheritdoc cref="IStorageProvider.TryGetFileFromPathAsync"/>
public static Task<IStorageFile?> TryGetFileFromPath(this IStorageProvider provider, string filePath) public static Task<IStorageFile?> TryGetFileFromPathAsync(this IStorageProvider provider, string filePath)
{ {
return provider.TryGetFileFromPath(StorageProviderHelpers.FilePathToUri(filePath)); return provider.TryGetFileFromPathAsync(StorageProviderHelpers.FilePathToUri(filePath));
} }
/// <inheritdoc cref="IStorageProvider.TryGetFolderFromPath"/> /// <inheritdoc cref="IStorageProvider.TryGetFolderFromPathAsync"/>
public static Task<IStorageFolder?> TryGetFolderFromPath(this IStorageProvider provider, string folderPath) public static Task<IStorageFolder?> TryGetFolderFromPathAsync(this IStorageProvider provider, string folderPath)
{ {
return provider.TryGetFolderFromPath(StorageProviderHelpers.FilePathToUri(folderPath)); return provider.TryGetFolderFromPathAsync(StorageProviderHelpers.FilePathToUri(folderPath));
} }
internal static string? TryGetFullPath(this IStorageFolder folder) /// <summary>
/// Gets the local file system path of the item as a string.
/// </summary>
/// <param name="item">Storage folder or file.</param>
/// <returns>Full local path to the folder or file if possible, otherwise null.</returns>
/// <remarks>
/// Android platform usually uses "content:" virtual file paths
/// and Browser platform has isolated access without full paths,
/// so on these platforms this method will return null.
/// </remarks>
public static string? TryGetLocalPath(this IStorageItem item)
{ {
// We can avoid double escaping of the path by checking for BclStorageFolder. // We can avoid double escaping of the path by checking for BclStorageFolder.
// Ideally, `folder.Path.LocalPath` should also work, as that's only available way for the users. // Ideally, `folder.Path.LocalPath` should also work, as that's only available way for the users.
if (folder is BclStorageFolder storageFolder) if (item is BclStorageFolder storageFolder)
{ {
return storageFolder.DirectoryInfo.FullName; return storageFolder.DirectoryInfo.FullName;
} }
if (item is BclStorageFile storageFile)
if (folder.Path is { IsAbsoluteUri: true, Scheme: "file" } absolutePath)
{
return absolutePath.LocalPath;
}
// android "content:", browser and ios relative links go here.
return null;
}
internal static string? TryGetFullPath(this IStorageFile file)
{
if (file is BclStorageFile storageFolder)
{ {
return storageFolder.FileInfo.FullName; return storageFile.FileInfo.FullName;
} }
if (file.Path is { IsAbsoluteUri: true, Scheme: "file" } absolutePath) if (item.Path is { IsAbsoluteUri: true, Scheme: "file" } absolutePath)
{ {
return absolutePath.LocalPath; return absolutePath.LocalPath;
} }
// android "content:", browser and ios relative links go here.
return null; return null;
} }
} }

7
src/Avalonia.Base/PropertyStore/EffectiveValue`1.cs

@ -19,13 +19,16 @@ namespace Avalonia.PropertyStore
private T? _baseValue; private T? _baseValue;
private UncommonFields? _uncommon; private UncommonFields? _uncommon;
public EffectiveValue(AvaloniaObject owner, StyledProperty<T> property) public EffectiveValue(
AvaloniaObject owner,
StyledProperty<T> property,
EffectiveValue<T>? inherited)
{ {
Priority = BindingPriority.Unset; Priority = BindingPriority.Unset;
BasePriority = BindingPriority.Unset; BasePriority = BindingPriority.Unset;
_metadata = property.GetMetadata(owner.GetType()); _metadata = property.GetMetadata(owner.GetType());
var value = _metadata.DefaultValue; var value = inherited is null ? _metadata.DefaultValue : inherited.Value;
if (property.HasCoercion && _metadata.CoerceValue is { } coerce) if (property.HasCoercion && _metadata.CoerceValue is { } coerce)
{ {

18
src/Avalonia.Base/PropertyStore/ValueStore.cs

@ -184,7 +184,7 @@ namespace Avalonia.PropertyStore
} }
else else
{ {
var effectiveValue = new EffectiveValue<T>(Owner, property); var effectiveValue = CreateEffectiveValue(property);
AddEffectiveValue(property, effectiveValue); AddEffectiveValue(property, effectiveValue);
effectiveValue.SetAndRaise(this, result, priority); effectiveValue.SetAndRaise(this, result, priority);
} }
@ -200,7 +200,7 @@ namespace Avalonia.PropertyStore
} }
else else
{ {
var effectiveValue = new EffectiveValue<T>(Owner, property); var effectiveValue = CreateEffectiveValue(property);
AddEffectiveValue(property, effectiveValue); AddEffectiveValue(property, effectiveValue);
effectiveValue.SetLocalValueAndRaise(this, property, value); effectiveValue.SetLocalValueAndRaise(this, property, value);
} }
@ -217,7 +217,7 @@ namespace Avalonia.PropertyStore
} }
else else
{ {
var effectiveValue = new EffectiveValue<T>(Owner, property); var effectiveValue = CreateEffectiveValue(property);
AddEffectiveValue(property, effectiveValue); AddEffectiveValue(property, effectiveValue);
effectiveValue.SetCurrentValueAndRaise(this, property, value); effectiveValue.SetCurrentValueAndRaise(this, property, value);
} }
@ -287,6 +287,16 @@ namespace Avalonia.PropertyStore
return false; return false;
} }
public EffectiveValue<T> CreateEffectiveValue<T>(StyledProperty<T> property)
{
EffectiveValue<T>? inherited = null;
if (property.Inherits && TryGetInheritedValue(property, out var v))
inherited = (EffectiveValue<T>)v;
return new EffectiveValue<T>(Owner, property, inherited);
}
public void SetInheritanceParent(AvaloniaObject? newParent) public void SetInheritanceParent(AvaloniaObject? newParent)
{ {
var values = AvaloniaPropertyDictionaryPool<OldNewValue>.Get(); var values = AvaloniaPropertyDictionaryPool<OldNewValue>.Get();
@ -799,7 +809,7 @@ namespace Avalonia.PropertyStore
// - The value is a non-animation value and its priority is higher than the current // - The value is a non-animation value and its priority is higher than the current
// effective value's base priority // effective value's base priority
var isRelevantPriority = current is null || var isRelevantPriority = current is null ||
priority < current.Priority || (priority < current.Priority && priority < current.BasePriority) ||
(priority > BindingPriority.Animation && priority < current.BasePriority); (priority > BindingPriority.Animation && priority < current.BasePriority);
if (foundEntry && isRelevantPriority && entry!.HasValue) if (foundEntry && isRelevantPriority && entry!.HasValue)

5
src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual.cs

@ -48,7 +48,8 @@ namespace Avalonia.Rendering.Composition.Server
{ {
canvas.PostTransform = Matrix.Identity; canvas.PostTransform = Matrix.Identity;
canvas.Transform = Matrix.Identity; canvas.Transform = Matrix.Identity;
canvas.PushClip(AdornedVisual._combinedTransformedClipBounds); if (AdornerIsClipped)
canvas.PushClip(AdornedVisual._combinedTransformedClipBounds);
} }
var transform = GlobalTransformMatrix; var transform = GlobalTransformMatrix;
canvas.PostTransform = MatrixUtils.ToMatrix(transform); canvas.PostTransform = MatrixUtils.ToMatrix(transform);
@ -74,7 +75,7 @@ namespace Avalonia.Rendering.Composition.Server
canvas.PopGeometryClip(); canvas.PopGeometryClip();
if (ClipToBounds && !HandlesClipToBounds) if (ClipToBounds && !HandlesClipToBounds)
canvas.PopClip(); canvas.PopClip();
if (AdornedVisual != null) if (AdornedVisual != null && AdornerIsClipped)
canvas.PopClip(); canvas.PopClip();
if(Opacity != 1) if(Opacity != 1)
canvas.PopOpacity(); canvas.PopOpacity();

8
src/Avalonia.Base/StyledElement.cs

@ -524,13 +524,7 @@ namespace Avalonia
NotifyResourcesChanged(); NotifyResourcesChanged();
} }
#nullable disable RaisePropertyChanged(ParentProperty, old, Parent);
RaisePropertyChanged(
ParentProperty,
new Optional<StyledElement>(old),
new BindingValue<StyledElement>(Parent),
BindingPriority.LocalValue);
#nullable enable
} }
} }

2
src/Avalonia.Base/StyledProperty.cs

@ -171,7 +171,7 @@ namespace Avalonia
internal override EffectiveValue CreateEffectiveValue(AvaloniaObject o) internal override EffectiveValue CreateEffectiveValue(AvaloniaObject o)
{ {
return new EffectiveValue<TValue>(o, this); return o.GetValueStore().CreateEffectiveValue(this);
} }
/// <inheritdoc/> /// <inheritdoc/>

24
src/Avalonia.Base/Utilities/WeakEvents.cs

@ -2,6 +2,7 @@ using System;
using System.Collections.Specialized; using System.Collections.Specialized;
using System.ComponentModel; using System.ComponentModel;
using System.Windows.Input; using System.Windows.Input;
using Avalonia.Threading;
namespace Avalonia.Utilities; namespace Avalonia.Utilities;
@ -20,15 +21,30 @@ public class WeakEvents
}); });
/// <summary> /// <summary>
/// Represents PropertyChanged event from <see cref="INotifyPropertyChanged"/> /// Represents PropertyChanged event from <see cref="INotifyPropertyChanged"/> with auto-dispatching to the UI thread
/// </summary> /// </summary>
public static readonly WeakEvent<INotifyPropertyChanged, PropertyChangedEventArgs> public static readonly WeakEvent<INotifyPropertyChanged, PropertyChangedEventArgs>
PropertyChanged = WeakEvent.Register<INotifyPropertyChanged, PropertyChangedEventArgs>( ThreadSafePropertyChanged = WeakEvent.Register<INotifyPropertyChanged, PropertyChangedEventArgs>(
(s, h) => (s, h) =>
{ {
PropertyChangedEventHandler handler = (_, e) => h(s, e); bool unsubscribed = false;
PropertyChangedEventHandler handler = (_, e) =>
{
if (Dispatcher.UIThread.CheckAccess())
h(s, e);
else
Dispatcher.UIThread.Post(() =>
{
if (!unsubscribed)
h(s, e);
});
};
s.PropertyChanged += handler; s.PropertyChanged += handler;
return () => s.PropertyChanged -= handler; return () =>
{
unsubscribed = true;
s.PropertyChanged -= handler;
};
}); });

2
src/Avalonia.Base/Visual.cs

@ -573,7 +573,7 @@ namespace Avalonia
/// <param name="newParent">The new visual parent.</param> /// <param name="newParent">The new visual parent.</param>
protected virtual void OnVisualParentChanged(Visual? oldParent, Visual? newParent) protected virtual void OnVisualParentChanged(Visual? oldParent, Visual? newParent)
{ {
RaisePropertyChanged(VisualParentProperty, oldParent, newParent, BindingPriority.LocalValue); RaisePropertyChanged(VisualParentProperty, oldParent, newParent);
} }
internal override ParametrizedLogger? GetBindingWarningLogger( internal override ParametrizedLogger? GetBindingWarningLogger(

1
src/Avalonia.Base/composition-schema.xml

@ -26,6 +26,7 @@
<Property Name="Scale" Type="Vector3" DefaultValue="new Vector3(1, 1, 1)" Animated="true"/> <Property Name="Scale" Type="Vector3" DefaultValue="new Vector3(1, 1, 1)" Animated="true"/>
<Property Name="TransformMatrix" Type="Matrix4x4" DefaultValue="Matrix4x4.Identity" Animated="true"/> <Property Name="TransformMatrix" Type="Matrix4x4" DefaultValue="Matrix4x4.Identity" Animated="true"/>
<Property Name="AdornedVisual" Type="CompositionVisual?" Internal="true" /> <Property Name="AdornedVisual" Type="CompositionVisual?" Internal="true" />
<Property Name="AdornerIsClipped" Type="bool" Internal="true" />
<Property Name="OpacityMaskBrush" Type="Avalonia.Media.IBrush?" Internal="true" /> <Property Name="OpacityMaskBrush" Type="Avalonia.Media.IBrush?" Internal="true" />
</Object> </Object>
<Object Name="CompositionContainerVisual" Inherits="CompositionVisual"/> <Object Name="CompositionContainerVisual" Inherits="CompositionVisual"/>

22
src/Avalonia.Controls/Automation/Peers/SliderAutomationPeer.cs

@ -0,0 +1,22 @@
using Avalonia.Automation.Peers;
namespace Avalonia.Controls.Automation.Peers
{
public class SliderAutomationPeer : RangeBaseAutomationPeer
{
public SliderAutomationPeer(Slider owner) : base(owner)
{
}
override protected string GetClassNameCore()
{
return "Slider";
}
override protected AutomationControlType GetAutomationControlTypeCore()
{
return AutomationControlType.Slider;
}
}
}

6
src/Avalonia.Controls/Platform/Dialogs/SystemDialogImpl.cs

@ -27,7 +27,7 @@ namespace Avalonia.Controls.Platform
var files = await filePicker.OpenFilePickerAsync(options); var files = await filePicker.OpenFilePickerAsync(options);
return files return files
.Select(file => file.TryGetFullPath() ?? file.Name) .Select(file => file.TryGetLocalPath() ?? file.Name)
.ToArray(); .ToArray();
} }
else if (dialog is SaveFileDialog saveDialog) else if (dialog is SaveFileDialog saveDialog)
@ -46,7 +46,7 @@ namespace Avalonia.Controls.Platform
return null; return null;
} }
var filePath = file.TryGetFullPath() ?? file.Name; var filePath = file.TryGetLocalPath() ?? file.Name;
return new[] { filePath }; return new[] { filePath };
} }
return null; return null;
@ -64,7 +64,7 @@ namespace Avalonia.Controls.Platform
var folders = await filePicker.OpenFolderPickerAsync(options); var folders = await filePicker.OpenFolderPickerAsync(options);
return folders return folders
.Select(folder => folder.TryGetFullPath() ?? folder.Name) .Select(folder => folder.TryGetLocalPath() ?? folder.Name)
.FirstOrDefault(u => u is not null); .FirstOrDefault(u => u is not null);
} }
} }

5
src/Avalonia.Controls/Primitives/AdornerLayer.cs

@ -279,8 +279,11 @@ namespace Avalonia.Controls.Primitives
private void UpdateAdornedElement(Visual adorner, Visual? adorned) private void UpdateAdornedElement(Visual adorner, Visual? adorned)
{ {
if (adorner.CompositionVisual != null) if (adorner.CompositionVisual != null)
{
adorner.CompositionVisual.AdornedVisual = adorned?.CompositionVisual; adorner.CompositionVisual.AdornedVisual = adorned?.CompositionVisual;
adorner.CompositionVisual.AdornerIsClipped = GetIsClipEnabled(adorner);
}
var info = adorner.GetValue(s_adornedElementInfoProperty); var info = adorner.GetValue(s_adornedElementInfoProperty);
if (info != null) if (info != null)

10
src/Avalonia.Controls/Primitives/SelectingItemsControl.cs

@ -345,10 +345,7 @@ namespace Avalonia.Controls.Primitives
if (_oldSelectedItems != SelectedItems) if (_oldSelectedItems != SelectedItems)
{ {
RaisePropertyChanged( RaisePropertyChanged(SelectedItemsProperty, _oldSelectedItems, SelectedItems);
SelectedItemsProperty,
new Optional<IList?>(_oldSelectedItems),
new BindingValue<IList?>(SelectedItems));
_oldSelectedItems = SelectedItems; _oldSelectedItems = SelectedItems;
} }
} }
@ -909,10 +906,7 @@ namespace Avalonia.Controls.Primitives
else if (e.PropertyName == nameof(InternalSelectionModel.WritableSelectedItems) && else if (e.PropertyName == nameof(InternalSelectionModel.WritableSelectedItems) &&
_oldSelectedItems != (Selection as InternalSelectionModel)?.SelectedItems) _oldSelectedItems != (Selection as InternalSelectionModel)?.SelectedItems)
{ {
RaisePropertyChanged( RaisePropertyChanged(SelectedItemsProperty, _oldSelectedItems, SelectedItems);
SelectedItemsProperty,
new Optional<IList?>(_oldSelectedItems),
new BindingValue<IList?>(SelectedItems));
_oldSelectedItems = SelectedItems; _oldSelectedItems = SelectedItems;
} }
else if (e.PropertyName == nameof(ISelectionModel.Source)) else if (e.PropertyName == nameof(ISelectionModel.Source))

39
src/Avalonia.Controls/RelativePanel.AttachedProperties.cs

@ -1,37 +1,30 @@
using Avalonia.Layout; using Avalonia.Layout;
using Avalonia.Threading;
namespace Avalonia.Controls namespace Avalonia.Controls
{ {
public partial class RelativePanel public partial class RelativePanel
{ {
private static void OnAlignPropertiesChanged(AvaloniaObject d, AvaloniaPropertyChangedEventArgs e)
{
if (d is Layoutable layoutable && layoutable.Parent is Layoutable layoutableParent)
{
layoutableParent.InvalidateArrange();
}
}
static RelativePanel() static RelativePanel()
{ {
ClipToBoundsProperty.OverrideDefaultValue<RelativePanel>(true); ClipToBoundsProperty.OverrideDefaultValue<RelativePanel>(true);
AboveProperty.Changed.AddClassHandler<Layoutable>(OnAlignPropertiesChanged); AffectsParentArrange<RelativePanel>(
AlignBottomWithPanelProperty.Changed.AddClassHandler<Layoutable>(OnAlignPropertiesChanged); AlignLeftWithPanelProperty, AlignLeftWithProperty, LeftOfProperty,
AlignBottomWithProperty.Changed.AddClassHandler<Layoutable>(OnAlignPropertiesChanged); AlignRightWithPanelProperty, AlignRightWithProperty, RightOfProperty,
AlignHorizontalCenterWithPanelProperty.Changed.AddClassHandler<Layoutable>(OnAlignPropertiesChanged); AlignTopWithPanelProperty, AlignTopWithProperty, AboveProperty,
AlignHorizontalCenterWithProperty.Changed.AddClassHandler<Layoutable>(OnAlignPropertiesChanged); AlignBottomWithPanelProperty, AlignBottomWithProperty, BelowProperty,
AlignLeftWithPanelProperty.Changed.AddClassHandler<Layoutable>(OnAlignPropertiesChanged); AlignHorizontalCenterWithPanelProperty, AlignHorizontalCenterWithProperty,
AlignLeftWithProperty.Changed.AddClassHandler<Layoutable>(OnAlignPropertiesChanged); AlignVerticalCenterWithPanelProperty, AlignVerticalCenterWithProperty);
AlignRightWithPanelProperty.Changed.AddClassHandler<Layoutable>(OnAlignPropertiesChanged);
AlignRightWithProperty.Changed.AddClassHandler<Layoutable>(OnAlignPropertiesChanged); AffectsParentMeasure<RelativePanel>(
AlignTopWithPanelProperty.Changed.AddClassHandler<Layoutable>(OnAlignPropertiesChanged); AlignLeftWithPanelProperty, AlignLeftWithProperty, LeftOfProperty,
AlignTopWithProperty.Changed.AddClassHandler<Layoutable>(OnAlignPropertiesChanged); AlignRightWithPanelProperty, AlignRightWithProperty, RightOfProperty,
AlignVerticalCenterWithPanelProperty.Changed.AddClassHandler<Layoutable>(OnAlignPropertiesChanged); AlignTopWithPanelProperty, AlignTopWithProperty, AboveProperty,
AlignVerticalCenterWithProperty.Changed.AddClassHandler<Layoutable>(OnAlignPropertiesChanged); AlignBottomWithPanelProperty, AlignBottomWithProperty, BelowProperty,
BelowProperty.Changed.AddClassHandler<Layoutable>(OnAlignPropertiesChanged); AlignHorizontalCenterWithPanelProperty, AlignHorizontalCenterWithProperty,
LeftOfProperty.Changed.AddClassHandler<Layoutable>(OnAlignPropertiesChanged); AlignVerticalCenterWithPanelProperty, AlignVerticalCenterWithProperty);
RightOfProperty.Changed.AddClassHandler<Layoutable>(OnAlignPropertiesChanged);
} }
/// <summary> /// <summary>

6
src/Avalonia.Controls/Slider.cs

@ -10,6 +10,7 @@ using Avalonia.Interactivity;
using Avalonia.Layout; using Avalonia.Layout;
using Avalonia.Utilities; using Avalonia.Utilities;
using Avalonia.Automation; using Avalonia.Automation;
using Avalonia.Controls.Automation.Peers;
namespace Avalonia.Controls namespace Avalonia.Controls
{ {
@ -380,6 +381,11 @@ namespace Avalonia.Controls
} }
} }
protected override AutomationPeer OnCreateAutomationPeer()
{
return new SliderAutomationPeer(this);
}
/// <inheritdoc /> /// <inheritdoc />
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
{ {

2
src/Avalonia.Diagnostics/Diagnostics/Controls/FilterTextBox.axaml

@ -16,6 +16,8 @@
<StackPanel Orientation="Horizontal" Spacing="1"> <StackPanel Orientation="Horizontal" Spacing="1">
<Button Margin="0,0,2,0" <Button Margin="0,0,2,0"
Classes="textBoxClearButton" Classes="textBoxClearButton"
Theme="{StaticResource SimpleTextBoxClearButtonTheme}"
Focusable="False"
ToolTip.Tip="Clear" ToolTip.Tip="Clear"
Cursor="Hand" Cursor="Hand"
Command="{Binding $parent[TextBox].Clear}" Command="{Binding $parent[TextBox].Clear}"

8
src/Avalonia.Diagnostics/Diagnostics/Screenshots/FilePickerHandler.cs

@ -54,8 +54,8 @@ namespace Avalonia.Diagnostics.Screenshots
protected override async Task<Stream?> GetStream(Control control) protected override async Task<Stream?> GetStream(Control control)
{ {
var storageProvider = GetTopLevel(control).StorageProvider; var storageProvider = GetTopLevel(control).StorageProvider;
var defaultFolder = await storageProvider.TryGetFolderFromPath(_screenshotRoot) var defaultFolder = await storageProvider.TryGetFolderFromPathAsync(_screenshotRoot)
?? await storageProvider.TryGetWellKnownFolder(WellKnownFolder.Pictures); ?? await storageProvider.TryGetWellKnownFolderAsync(WellKnownFolder.Pictures);
var result = await storageProvider.SaveFilePickerAsync(new FilePickerSaveOptions var result = await storageProvider.SaveFilePickerAsync(new FilePickerSaveOptions
{ {
@ -68,10 +68,6 @@ namespace Avalonia.Diagnostics.Screenshots
{ {
return null; return null;
} }
if (!result.CanOpenWrite)
{
throw new InvalidOperationException("Read-only file was selected.");
}
return await result.OpenWriteAsync(); return await result.OpenWriteAsync();
} }

4
src/Avalonia.Diagnostics/Diagnostics/ViewModels/ControlLayoutViewModel.cs

@ -103,13 +103,13 @@ namespace Avalonia.Diagnostics.ViewModels
public HorizontalAlignment HorizontalAlignment public HorizontalAlignment HorizontalAlignment
{ {
get => _horizontalAlignment; get => _horizontalAlignment;
private set => RaiseAndSetIfChanged(ref _horizontalAlignment, value); set => RaiseAndSetIfChanged(ref _horizontalAlignment, value);
} }
public VerticalAlignment VerticalAlignment public VerticalAlignment VerticalAlignment
{ {
get => _verticalAlignment; get => _verticalAlignment;
private set => RaiseAndSetIfChanged(ref _verticalAlignment, value); set => RaiseAndSetIfChanged(ref _verticalAlignment, value);
} }
public bool HasPadding { get; } public bool HasPadding { get; }

2
src/Avalonia.Dialogs/Internal/ManagedFileChooserViewModel.cs

@ -260,7 +260,7 @@ namespace Avalonia.Dialogs.Internal
public void Navigate(IStorageFolder path, string initialSelectionName = null) public void Navigate(IStorageFolder path, string initialSelectionName = null)
{ {
var fullDirectoryPath = path?.TryGetFullPath() ?? Directory.GetCurrentDirectory(); var fullDirectoryPath = path?.TryGetLocalPath() ?? Directory.GetCurrentDirectory();
Navigate(fullDirectoryPath, initialSelectionName); Navigate(fullDirectoryPath, initialSelectionName);
} }

2
src/Avalonia.Dialogs/ManagedFileDialogExtensions.cs

@ -51,7 +51,7 @@ namespace Avalonia.Dialogs
var files = await impl.OpenFilePickerAsync(dialog.ToFilePickerOpenOptions()); var files = await impl.OpenFilePickerAsync(dialog.ToFilePickerOpenOptions());
return files return files
.Select(file => file.TryGetFullPath() ?? file.Name) .Select(file => file.TryGetLocalPath() ?? file.Name)
.ToArray(); .ToArray();
} }
} }

2
src/Avalonia.FreeDesktop/DBusSystemDialog.cs

@ -88,7 +88,7 @@ namespace Avalonia.FreeDesktop
if (options.SuggestedFileName is { } currentName) if (options.SuggestedFileName is { } currentName)
chooserOptions.Add("current_name", currentName); chooserOptions.Add("current_name", currentName);
if (options.SuggestedStartLocation?.TryGetFullPath() is { } folderPath) if (options.SuggestedStartLocation?.TryGetLocalPath() is { } folderPath)
chooserOptions.Add("current_folder", Encoding.UTF8.GetBytes(folderPath)); chooserOptions.Add("current_folder", Encoding.UTF8.GetBytes(folderPath));
objectPath = await _fileChooser.SaveFileAsync(parentWindow, options.Title ?? string.Empty, chooserOptions); objectPath = await _fileChooser.SaveFileAsync(parentWindow, options.Title ?? string.Empty, chooserOptions);

2
src/Avalonia.Headless/AvaloniaHeadlessPlatform.cs

@ -68,8 +68,6 @@ namespace Avalonia.Headless
.Bind<IKeyboardDevice>().ToConstant(new KeyboardDevice()) .Bind<IKeyboardDevice>().ToConstant(new KeyboardDevice())
.Bind<IRenderLoop>().ToConstant(new RenderLoop()) .Bind<IRenderLoop>().ToConstant(new RenderLoop())
.Bind<IRenderTimer>().ToConstant(new RenderTimer(60)) .Bind<IRenderTimer>().ToConstant(new RenderTimer(60))
.Bind<IFontManagerImpl>().ToSingleton<HeadlessFontManagerStub>()
.Bind<ITextShaperImpl>().ToSingleton<HeadlessTextShaperStub>()
.Bind<IWindowingPlatform>().ToConstant(new HeadlessWindowingPlatform()) .Bind<IWindowingPlatform>().ToConstant(new HeadlessWindowingPlatform())
.Bind<PlatformHotkeyConfiguration>().ToSingleton<PlatformHotkeyConfiguration>(); .Bind<PlatformHotkeyConfiguration>().ToSingleton<PlatformHotkeyConfiguration>();
Compositor = new Compositor(AvaloniaLocator.Current.GetRequiredService<IRenderLoop>(), null); Compositor = new Compositor(AvaloniaLocator.Current.GetRequiredService<IRenderLoop>(), null);

6
src/Avalonia.Native/SystemDialogs.cs

@ -33,7 +33,7 @@ namespace Avalonia.Native
{ {
using var events = new SystemDialogEvents(); using var events = new SystemDialogEvents();
var suggestedDirectory = options.SuggestedStartLocation?.TryGetFullPath() ?? string.Empty; var suggestedDirectory = options.SuggestedStartLocation?.TryGetLocalPath() ?? string.Empty;
_native.OpenFileDialog((IAvnWindow)_window.Native, _native.OpenFileDialog((IAvnWindow)_window.Native,
events, events,
@ -53,7 +53,7 @@ namespace Avalonia.Native
{ {
using var events = new SystemDialogEvents(); using var events = new SystemDialogEvents();
var suggestedDirectory = options.SuggestedStartLocation?.TryGetFullPath() ?? string.Empty; var suggestedDirectory = options.SuggestedStartLocation?.TryGetLocalPath() ?? string.Empty;
_native.SaveFileDialog((IAvnWindow)_window.Native, _native.SaveFileDialog((IAvnWindow)_window.Native,
events, events,
@ -72,7 +72,7 @@ namespace Avalonia.Native
{ {
using var events = new SystemDialogEvents(); using var events = new SystemDialogEvents();
var suggestedDirectory = options.SuggestedStartLocation?.TryGetFullPath() ?? string.Empty; var suggestedDirectory = options.SuggestedStartLocation?.TryGetLocalPath() ?? string.Empty;
_native.SelectFolderDialog((IAvnWindow)_window.Native, events, options.AllowMultiple.AsComBool(), options.Title ?? "", suggestedDirectory); _native.SelectFolderDialog((IAvnWindow)_window.Native, events, options.AllowMultiple.AsComBool(), options.Title ?? "", suggestedDirectory);

1
src/Avalonia.OpenGL/Avalonia.OpenGL.csproj

@ -13,6 +13,7 @@
<Import Project="..\..\build\DevAnalyzers.props" /> <Import Project="..\..\build\DevAnalyzers.props" />
<Import Project="..\..\build\SourceGenerators.props" /> <Import Project="..\..\build\SourceGenerators.props" />
<Import Project="..\..\build\TrimmingEnable.props" /> <Import Project="..\..\build\TrimmingEnable.props" />
<Import Project="..\..\build\NullableEnable.props" />
<ItemGroup> <ItemGroup>
<Compile Remove="..\Shared\SourceGeneratorAttributes.cs" /> <Compile Remove="..\Shared\SourceGeneratorAttributes.cs" />

5
src/Avalonia.OpenGL/Controls/CompositionOpenGlSwapchain.cs

@ -1,5 +1,4 @@
using System; using System;
using System.Collections.Generic;
using System.Threading.Tasks; using System.Threading.Tasks;
using Avalonia.Platform; using Avalonia.Platform;
using Avalonia.Rendering; using Avalonia.Rendering;
@ -22,7 +21,7 @@ internal class CompositionOpenGlSwapchain : SwapchainBase<IGlSwapchainImage>
} }
public CompositionOpenGlSwapchain(IGlContext context, ICompositionGpuInterop interop, CompositionDrawingSurface target, public CompositionOpenGlSwapchain(IGlContext context, ICompositionGpuInterop interop, CompositionDrawingSurface target,
IGlContextExternalObjectsFeature externalObjectsFeature) : base(interop, target) IGlContextExternalObjectsFeature? externalObjectsFeature) : base(interop, target)
{ {
_context = context; _context = context;
_externalObjectsFeature = externalObjectsFeature; _externalObjectsFeature = externalObjectsFeature;
@ -95,7 +94,7 @@ internal class DxgiMutexOpenGlSwapChainImage : IGlSwapchainImage
public int TextureId => _texture.TextureId; public int TextureId => _texture.TextureId;
public int InternalFormat => _texture.InternalFormat; public int InternalFormat => _texture.InternalFormat;
public PixelSize Size => new(_texture.Properties.Width, _texture.Properties.Height); public PixelSize Size => new(_texture.Properties.Width, _texture.Properties.Height);
public Task LastPresent => _lastPresent; public Task? LastPresent => _lastPresent;
public void BeginDraw() => _texture.AcquireKeyedMutex(0); public void BeginDraw() => _texture.AcquireKeyedMutex(0);
public void Present() public void Present()

33
src/Avalonia.OpenGL/Controls/OpenGlControlBase.cs

@ -1,4 +1,5 @@
using System; using System;
using System.Diagnostics.CodeAnalysis;
using System.Numerics; using System.Numerics;
using System.Threading.Tasks; using System.Threading.Tasks;
using Avalonia.Controls; using Avalonia.Controls;
@ -11,10 +12,10 @@ namespace Avalonia.OpenGL.Controls
{ {
public abstract class OpenGlControlBase : Control public abstract class OpenGlControlBase : Control
{ {
private CompositionSurfaceVisual _visual; private CompositionSurfaceVisual? _visual;
private Action _update; private readonly Action _update;
private bool _updateQueued; private bool _updateQueued;
private Task<bool> _initialization; private Task<bool>? _initialization;
private OpenGlControlBaseResources? _resources; private OpenGlControlBaseResources? _resources;
private Compositor? _compositor; private Compositor? _compositor;
protected GlVersion GlVersion => _resources?.Context.Version ?? default; protected GlVersion GlVersion => _resources?.Context.Version ?? default;
@ -24,7 +25,7 @@ namespace Avalonia.OpenGL.Controls
_update = Update; _update = Update;
} }
void DoCleanup() private void DoCleanup()
{ {
if (_initialization is { Status: TaskStatus.RanToCompletion } && _resources != null) if (_initialization is { Status: TaskStatus.RanToCompletion } && _resources != null)
{ {
@ -63,14 +64,14 @@ namespace Avalonia.OpenGL.Controls
RequestNextFrameRendering(); RequestNextFrameRendering();
} }
[MemberNotNullWhen(true, nameof(_resources))]
private bool EnsureInitializedCore( private bool EnsureInitializedCore(
ICompositionGpuInterop interop, ICompositionGpuInterop interop,
IOpenGlTextureSharingRenderInterfaceContextFeature contextSharingFeature) IOpenGlTextureSharingRenderInterfaceContextFeature? contextSharingFeature)
{ {
var surface = _compositor.CreateDrawingSurface(); var surface = _compositor!.CreateDrawingSurface();
IGlContext ctx = null; IGlContext? ctx = null;
var contextFactory = AvaloniaLocator.Current.GetService<IPlatformGraphicsOpenGlContextFactory>();
try try
{ {
if (contextSharingFeature?.CanCreateSharedContext == true) if (contextSharingFeature?.CanCreateSharedContext == true)
@ -78,6 +79,7 @@ namespace Avalonia.OpenGL.Controls
if(_resources == null) if(_resources == null)
{ {
var contextFactory = AvaloniaLocator.Current.GetRequiredService<IPlatformGraphicsOpenGlContextFactory>();
ctx = contextFactory.CreateContext(null); ctx = contextFactory.CreateContext(null);
if (ctx.TryGetFeature<IGlContextExternalObjectsFeature>(out var externalObjects)) if (ctx.TryGetFeature<IGlContextExternalObjectsFeature>(out var externalObjects))
_resources = OpenGlControlBaseResources.TryCreate(ctx, surface, interop, externalObjects); _resources = OpenGlControlBaseResources.TryCreate(ctx, surface, interop, externalObjects);
@ -121,13 +123,14 @@ namespace Avalonia.OpenGL.Controls
base.OnPropertyChanged(change); base.OnPropertyChanged(change);
} }
void ContextLost() private void ContextLost()
{ {
_initialization = null; _initialization = null;
_resources?.DisposeAsync(); _resources?.DisposeAsync();
OnOpenGlLost(); OnOpenGlLost();
} }
[MemberNotNullWhen(true, nameof(_resources))]
private bool EnsureInitialized() private bool EnsureInitialized()
{ {
if (_initialization != null) if (_initialization != null)
@ -170,11 +173,11 @@ namespace Avalonia.OpenGL.Controls
private void Update() private void Update()
{ {
_updateQueued = false; _updateQueued = false;
if (VisualRoot == null) if (VisualRoot is not { } visualRoot)
return; return;
if(!EnsureInitialized()) if(!EnsureInitialized())
return; return;
using (_resources.BeginDraw(GetPixelSize())) using (_resources.BeginDraw(GetPixelSize(visualRoot)))
OnOpenGlRender(_resources.Context.GlInterface, _resources.Fbo); OnOpenGlRender(_resources.Context.GlInterface, _resources.Fbo);
} }
@ -190,7 +193,7 @@ namespace Avalonia.OpenGL.Controls
var gpuInteropTask = _compositor.TryGetCompositionGpuInterop(); var gpuInteropTask = _compositor.TryGetCompositionGpuInterop();
var contextSharingFeature = var contextSharingFeature =
(IOpenGlTextureSharingRenderInterfaceContextFeature) (IOpenGlTextureSharingRenderInterfaceContextFeature?)
await _compositor.TryGetRenderInterfaceFeature( await _compositor.TryGetRenderInterfaceFeature(
typeof(IOpenGlTextureSharingRenderInterfaceContextFeature)); typeof(IOpenGlTextureSharingRenderInterfaceContextFeature));
var interop = await gpuInteropTask; var interop = await gpuInteropTask;
@ -208,7 +211,7 @@ namespace Avalonia.OpenGL.Controls
return false; return false;
} }
using (_resources!.Context.MakeCurrent()) using (_resources.Context.MakeCurrent())
OnOpenGlInit(_resources.Context.GlInterface); OnOpenGlInit(_resources.Context.GlInterface);
return true; return true;
@ -228,9 +231,9 @@ namespace Avalonia.OpenGL.Controls
} }
} }
private PixelSize GetPixelSize() private PixelSize GetPixelSize(IRenderRoot visualRoot)
{ {
var scaling = VisualRoot!.RenderScaling; var scaling = visualRoot.RenderScaling;
return new PixelSize(Math.Max(1, (int)(Bounds.Width * scaling)), return new PixelSize(Math.Max(1, (int)(Bounds.Width * scaling)),
Math.Max(1, (int)(Bounds.Height * scaling))); Math.Max(1, (int)(Bounds.Height * scaling)));
} }

6
src/Avalonia.OpenGL/Controls/OpenGlControlResources.cs

@ -14,14 +14,14 @@ internal class OpenGlControlBaseResources : IAsyncDisposable
public int Fbo { get; private set; } public int Fbo { get; private set; }
private PixelSize _depthBufferSize; private PixelSize _depthBufferSize;
public CompositionDrawingSurface Surface { get; } public CompositionDrawingSurface Surface { get; }
public CompositionOpenGlSwapchain _swapchain; private readonly CompositionOpenGlSwapchain _swapchain;
public IGlContext Context { get; private set; } public IGlContext Context { get; private set; }
public static OpenGlControlBaseResources? TryCreate(CompositionDrawingSurface surface, public static OpenGlControlBaseResources? TryCreate(CompositionDrawingSurface surface,
ICompositionGpuInterop interop, ICompositionGpuInterop interop,
IOpenGlTextureSharingRenderInterfaceContextFeature feature) IOpenGlTextureSharingRenderInterfaceContextFeature feature)
{ {
IGlContext context; IGlContext? context;
try try
{ {
context = feature.CreateSharedContext(); context = feature.CreateSharedContext();
@ -71,7 +71,7 @@ internal class OpenGlControlBaseResources : IAsyncDisposable
new CompositionOpenGlSwapchain(context, interop, Surface, externalObjects); new CompositionOpenGlSwapchain(context, interop, Surface, externalObjects);
} }
void UpdateDepthRenderbuffer(PixelSize size) private void UpdateDepthRenderbuffer(PixelSize size)
{ {
if (size == _depthBufferSize && _depthBuffer != 0) if (size == _depthBufferSize && _depthBuffer != 0)
return; return;

22
src/Avalonia.OpenGL/Egl/EglContext.cs

@ -12,15 +12,15 @@ namespace Avalonia.OpenGL.Egl
{ {
private readonly EglDisplay _disp; private readonly EglDisplay _disp;
private readonly EglInterface _egl; private readonly EglInterface _egl;
private readonly EglContext _sharedWith; private readonly EglContext? _sharedWith;
private bool _isLost; private bool _isLost;
private IntPtr _context; private IntPtr _context;
private readonly Action _disposeCallback; private readonly Action? _disposeCallback;
private readonly Dictionary<Type, object> _features; private readonly Dictionary<Type, object> _features;
private readonly object _lock; private readonly object _lock;
internal EglContext(EglDisplay display, EglInterface egl, EglContext sharedWith, IntPtr ctx, EglSurface offscreenSurface, internal EglContext(EglDisplay display, EglInterface egl, EglContext? sharedWith, IntPtr ctx, EglSurface? offscreenSurface,
GlVersion version, int sampleCount, int stencilSize, Action disposeCallback, GlVersion version, int sampleCount, int stencilSize, Action? disposeCallback,
Dictionary<Type, Func<EglContext, object>> features) Dictionary<Type, Func<EglContext, object>> features)
{ {
_disp = display; _disp = display;
@ -42,19 +42,19 @@ namespace Avalonia.OpenGL.Egl
public IntPtr Context => public IntPtr Context =>
_context == IntPtr.Zero ? throw new ObjectDisposedException(nameof(EglContext)) : _context; _context == IntPtr.Zero ? throw new ObjectDisposedException(nameof(EglContext)) : _context;
public EglSurface OffscreenSurface { get; } public EglSurface? OffscreenSurface { get; }
public GlVersion Version { get; } public GlVersion Version { get; }
public GlInterface GlInterface { get; } public GlInterface GlInterface { get; }
public int SampleCount { get; } public int SampleCount { get; }
public int StencilSize { get; } public int StencilSize { get; }
public EglDisplay Display => _disp; public EglDisplay Display => _disp;
class RestoreContext : IDisposable private class RestoreContext : IDisposable
{ {
private readonly EglInterface _egl; private readonly EglInterface _egl;
private readonly object _l; private readonly object _l;
private readonly IntPtr _display; private readonly IntPtr _display;
public IntPtr _context, _read, _draw; private readonly IntPtr _context, _read, _draw;
public RestoreContext(EglInterface egl, IntPtr defDisplay, object l) public RestoreContext(EglInterface egl, IntPtr defDisplay, object l)
{ {
@ -78,7 +78,7 @@ namespace Avalonia.OpenGL.Egl
public IDisposable MakeCurrent() => MakeCurrent(OffscreenSurface); public IDisposable MakeCurrent() => MakeCurrent(OffscreenSurface);
public IDisposable MakeCurrent(EglSurface surface) public IDisposable MakeCurrent(EglSurface? surface)
{ {
if (IsLost) if (IsLost)
throw new PlatformGraphicsContextLostException(); throw new PlatformGraphicsContextLostException();
@ -147,7 +147,7 @@ namespace Avalonia.OpenGL.Egl
public bool CanCreateSharedContext => _disp.SupportsSharing; public bool CanCreateSharedContext => _disp.SupportsSharing;
public IGlContext CreateSharedContext(IEnumerable<GlVersion> preferredVersions = null) => public IGlContext CreateSharedContext(IEnumerable<GlVersion>? preferredVersions = null) =>
_disp.CreateContext(new EglContextOptions _disp.CreateContext(new EglContextOptions
{ {
ShareWith = _sharedWith ?? this ShareWith = _sharedWith ?? this
@ -174,9 +174,9 @@ namespace Avalonia.OpenGL.Egl
_disposeCallback?.Invoke(); _disposeCallback?.Invoke();
} }
public object TryGetFeature(Type featureType) public object? TryGetFeature(Type featureType)
{ {
if (_features?.TryGetValue(featureType, out var feature) == true) if (_features.TryGetValue(featureType, out var feature))
return feature; return feature;
return null; return null;
} }

11
src/Avalonia.OpenGL/Egl/EglDisplay.cs

@ -14,14 +14,14 @@ namespace Avalonia.OpenGL.Egl
private readonly EglDisplayOptions _options; private readonly EglDisplayOptions _options;
private EglConfigInfo _config; private EglConfigInfo _config;
private bool _isLost; private bool _isLost;
private object _lock = new(); private readonly object _lock = new();
public bool SupportsSharing { get; } public bool SupportsSharing { get; }
public IntPtr Handle => _display; public IntPtr Handle => _display;
public IntPtr Config => _config.Config; public IntPtr Config => _config.Config;
internal bool SingleContext => !_options.SupportsMultipleContexts; internal bool SingleContext => !_options.SupportsMultipleContexts;
private List<EglContext> _contexts = new(); private readonly List<EglContext> _contexts = new();
public EglDisplay() : this(new EglDisplayCreationOptions public EglDisplay() : this(new EglDisplayCreationOptions
{ {
@ -38,7 +38,7 @@ namespace Avalonia.OpenGL.Egl
public EglDisplay(IntPtr display, EglDisplayOptions options) public EglDisplay(IntPtr display, EglDisplayOptions options)
{ {
_egl = options.Egl; _egl = options.Egl ?? new EglInterface();
SupportsSharing = options.SupportsContextSharing; SupportsSharing = options.SupportsContextSharing;
_display = display; _display = display;
_options = options; _options = options;
@ -49,7 +49,7 @@ namespace Avalonia.OpenGL.Egl
} }
public EglInterface EglInterface => _egl; public EglInterface EglInterface => _egl;
public EglContext CreateContext(EglContextOptions options) public EglContext CreateContext(EglContextOptions? options)
{ {
if (SingleContext && _contexts.Any()) if (SingleContext && _contexts.Any())
throw new OpenGlException("This EGLDisplay can only have one active context"); throw new OpenGlException("This EGLDisplay can only have one active context");
@ -129,7 +129,7 @@ namespace Avalonia.OpenGL.Egl
protected virtual bool DisplayLockIsSharedWithContexts => false; protected virtual bool DisplayLockIsSharedWithContexts => false;
internal object ContextSharedSyncRoot => DisplayLockIsSharedWithContexts ? _lock : null; internal object? ContextSharedSyncRoot => DisplayLockIsSharedWithContexts ? _lock : null;
internal void OnContextLost(EglContext context) internal void OnContextLost(EglContext context)
{ {
@ -171,7 +171,6 @@ namespace Avalonia.OpenGL.Egl
if (_display != IntPtr.Zero) if (_display != IntPtr.Zero)
_egl.Terminate(_display); _egl.Terminate(_display);
_display = IntPtr.Zero; _display = IntPtr.Zero;
_config = null;
_options.DisposeCallback?.Invoke(); _options.DisposeCallback?.Invoke();
} }
} }

18
src/Avalonia.OpenGL/Egl/EglDisplayOptions.cs

@ -5,26 +5,26 @@ namespace Avalonia.OpenGL.Egl;
public class EglDisplayOptions public class EglDisplayOptions
{ {
public EglInterface Egl { get; set; } public EglInterface? Egl { get; set; }
public bool SupportsContextSharing { get; set; } public bool SupportsContextSharing { get; set; }
public bool SupportsMultipleContexts { get; set; } public bool SupportsMultipleContexts { get; set; }
public bool ContextLossIsDisplayLoss { get; set; } public bool ContextLossIsDisplayLoss { get; set; }
public Func<bool> DeviceLostCheckCallback { get; set; } public Func<bool>? DeviceLostCheckCallback { get; set; }
public Action DisposeCallback { get; set; } public Action? DisposeCallback { get; set; }
public IEnumerable<GlVersion> GlVersions { get; set; } public IEnumerable<GlVersion>? GlVersions { get; set; }
} }
public class EglContextOptions public class EglContextOptions
{ {
public EglContext ShareWith { get; set; } public EglContext? ShareWith { get; set; }
public EglSurface OffscreenSurface { get; set; } public EglSurface? OffscreenSurface { get; set; }
public Action DisposeCallback { get; set; } public Action? DisposeCallback { get; set; }
public Dictionary<Type, Func<EglContext, object>> ExtraFeatures { get; set; } public Dictionary<Type, Func<EglContext, object>>? ExtraFeatures { get; set; }
} }
public class EglDisplayCreationOptions : EglDisplayOptions public class EglDisplayCreationOptions : EglDisplayOptions
{ {
public int? PlatformType { get; set; } public int? PlatformType { get; set; }
public IntPtr PlatformDisplay { get; set; } public IntPtr PlatformDisplay { get; set; }
public int[] PlatformDisplayAttrs { get; set; } public int[]? PlatformDisplayAttrs { get; set; }
} }

11
src/Avalonia.OpenGL/Egl/EglDisplayUtils.cs

@ -1,15 +1,14 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading;
using static Avalonia.OpenGL.Egl.EglConsts; using static Avalonia.OpenGL.Egl.EglConsts;
namespace Avalonia.OpenGL.Egl; namespace Avalonia.OpenGL.Egl;
static class EglDisplayUtils internal static class EglDisplayUtils
{ {
public static IntPtr CreateDisplay(EglDisplayCreationOptions options) public static IntPtr CreateDisplay(EglDisplayCreationOptions options)
{ {
var egl = options.Egl; var egl = options.Egl ?? new EglInterface();
var display = IntPtr.Zero; var display = IntPtr.Zero;
if (options.PlatformType == null) if (options.PlatformType == null)
{ {
@ -30,9 +29,9 @@ static class EglDisplayUtils
return display; return display;
} }
public static EglConfigInfo InitializeAndGetConfig(EglInterface egl, IntPtr display, IEnumerable<GlVersion> versions) public static EglConfigInfo InitializeAndGetConfig(EglInterface egl, IntPtr display, IEnumerable<GlVersion>? versions)
{ {
if (!egl.Initialize(display, out var major, out var minor)) if (!egl.Initialize(display, out _, out _))
throw OpenGlException.GetFormattedException("eglInitialize", egl); throw OpenGlException.GetFormattedException("eglInitialize", egl);
// TODO: AvaloniaLocator.Current.GetService<AngleOptions>()?.GlProfiles // TODO: AvaloniaLocator.Current.GetService<AngleOptions>()?.GlProfiles
@ -112,7 +111,7 @@ static class EglDisplayUtils
} }
class EglConfigInfo internal class EglConfigInfo
{ {
public IntPtr Config { get; } public IntPtr Config { get; }
public GlVersion Version { get; } public GlVersion Version { get; }

8
src/Avalonia.OpenGL/Egl/EglGlPlatformSurface.cs

@ -27,9 +27,9 @@ namespace Avalonia.OpenGL.Egl
return new RenderTarget(glSurface, eglContext, _info); return new RenderTarget(glSurface, eglContext, _info);
} }
class RenderTarget : EglPlatformSurfaceRenderTargetBase private class RenderTarget : EglPlatformSurfaceRenderTargetBase
{ {
private EglSurface _glSurface; private EglSurface? _glSurface;
private readonly IEglWindowGlPlatformSurfaceInfo _info; private readonly IEglWindowGlPlatformSurfaceInfo _info;
private PixelSize _currentSize; private PixelSize _currentSize;
private readonly IntPtr _handle; private readonly IntPtr _handle;
@ -42,12 +42,12 @@ namespace Avalonia.OpenGL.Egl
_handle = _info.Handle; _handle = _info.Handle;
} }
public override void Dispose() => _glSurface.Dispose(); public override void Dispose() => _glSurface?.Dispose();
public override IGlPlatformSurfaceRenderingSession BeginDrawCore() public override IGlPlatformSurfaceRenderingSession BeginDrawCore()
{ {
if (_info.Size != _currentSize if (_info.Size != _currentSize
|| _handle != _info.Handle || _handle != _info.Handle
|| _glSurface == null) || _glSurface == null)
{ {
_glSurface?.Dispose(); _glSurface?.Dispose();

11
src/Avalonia.OpenGL/Egl/EglGlPlatformSurfaceBase.cs

@ -33,7 +33,7 @@ namespace Avalonia.OpenGL.Egl
public abstract IGlPlatformSurfaceRenderingSession BeginDrawCore(); public abstract IGlPlatformSurfaceRenderingSession BeginDrawCore();
protected IGlPlatformSurfaceRenderingSession BeginDraw(EglSurface surface, protected IGlPlatformSurfaceRenderingSession BeginDraw(EglSurface surface,
PixelSize size, double scaling, Action onFinish = null, bool isYFlipped = false) PixelSize size, double scaling, Action? onFinish = null, bool isYFlipped = false)
{ {
var restoreContext = Context.MakeCurrent(surface); var restoreContext = Context.MakeCurrent(surface);
@ -56,19 +56,18 @@ namespace Avalonia.OpenGL.Egl
restoreContext.Dispose(); restoreContext.Dispose();
} }
} }
class Session : IGlPlatformSurfaceRenderingSession private class Session : IGlPlatformSurfaceRenderingSession
{ {
private readonly EglContext _context; private readonly EglContext _context;
private readonly EglSurface _glSurface; private readonly EglSurface _glSurface;
private readonly EglDisplay _display; private readonly EglDisplay _display;
private readonly IDisposable _restoreContext; private readonly IDisposable _restoreContext;
private readonly Action _onFinish; private readonly Action? _onFinish;
public Session(EglDisplay display, EglContext context, public Session(EglDisplay display, EglContext context,
EglSurface glSurface, PixelSize size, double scaling, EglSurface glSurface, PixelSize size, double scaling,
IDisposable restoreContext, Action onFinish, bool isYFlipped) IDisposable restoreContext, Action? onFinish, bool isYFlipped)
{ {
Size = size; Size = size;
Scaling = scaling; Scaling = scaling;

10
src/Avalonia.OpenGL/Egl/EglInterface.cs

@ -49,7 +49,7 @@ namespace Avalonia.OpenGL.Egl
public partial IntPtr GetDisplay(IntPtr nativeDisplay); public partial IntPtr GetDisplay(IntPtr nativeDisplay);
[GetProcAddress("eglGetPlatformDisplayEXT", true)] [GetProcAddress("eglGetPlatformDisplayEXT", true)]
public partial IntPtr GetPlatformDisplayExt(int platform, IntPtr nativeDisplay, int[] attrs); public partial IntPtr GetPlatformDisplayExt(int platform, IntPtr nativeDisplay, int[]? attrs);
[GetProcAddress("eglInitialize")] [GetProcAddress("eglInitialize")]
public partial bool Initialize(IntPtr display, out int major, out int minor); public partial bool Initialize(IntPtr display, out int major, out int minor);
@ -75,7 +75,7 @@ namespace Avalonia.OpenGL.Egl
public partial bool DestroyContext(IntPtr display, IntPtr context); public partial bool DestroyContext(IntPtr display, IntPtr context);
[GetProcAddress("eglCreatePbufferSurface")] [GetProcAddress("eglCreatePbufferSurface")]
public partial IntPtr CreatePBufferSurface(IntPtr display, IntPtr config, int[] attrs); public partial IntPtr CreatePBufferSurface(IntPtr display, IntPtr config, int[]? attrs);
[GetProcAddress("eglMakeCurrent")] [GetProcAddress("eglMakeCurrent")]
public partial bool MakeCurrent(IntPtr display, IntPtr draw, IntPtr read, IntPtr context); public partial bool MakeCurrent(IntPtr display, IntPtr draw, IntPtr read, IntPtr context);
@ -96,7 +96,7 @@ namespace Avalonia.OpenGL.Egl
public partial void SwapBuffers(IntPtr display, IntPtr surface); public partial void SwapBuffers(IntPtr display, IntPtr surface);
[GetProcAddress("eglCreateWindowSurface")] [GetProcAddress("eglCreateWindowSurface")]
public partial IntPtr CreateWindowSurface(IntPtr display, IntPtr config, IntPtr window, int[] attrs); public partial IntPtr CreateWindowSurface(IntPtr display, IntPtr config, IntPtr window, int[]? attrs);
[GetProcAddress("eglBindTexImage")] [GetProcAddress("eglBindTexImage")]
public partial int BindTexImage(IntPtr display, IntPtr surface, int buffer); public partial int BindTexImage(IntPtr display, IntPtr surface, int buffer);
@ -116,7 +116,7 @@ namespace Avalonia.OpenGL.Egl
[GetProcAddress("eglQueryString")] [GetProcAddress("eglQueryString")]
public partial IntPtr QueryStringNative(IntPtr display, int i); public partial IntPtr QueryStringNative(IntPtr display, int i);
public string QueryString(IntPtr display, int i) public string? QueryString(IntPtr display, int i)
{ {
var rv = QueryStringNative(display, i); var rv = QueryStringNative(display, i);
if (rv == IntPtr.Zero) if (rv == IntPtr.Zero)
@ -125,7 +125,7 @@ namespace Avalonia.OpenGL.Egl
} }
[GetProcAddress("eglCreatePbufferFromClientBuffer")] [GetProcAddress("eglCreatePbufferFromClientBuffer")]
public partial IntPtr CreatePbufferFromClientBuffer(IntPtr display, int buftype, IntPtr buffer, IntPtr config, int[] attrib_list); public partial IntPtr CreatePbufferFromClientBuffer(IntPtr display, int buftype, IntPtr buffer, IntPtr config, int[]? attrib_list);
[GetProcAddress("eglQueryDisplayAttribEXT", true)] [GetProcAddress("eglQueryDisplayAttribEXT", true)]
public partial bool QueryDisplayAttribExt(IntPtr display, int attr, out IntPtr res); public partial bool QueryDisplayAttribExt(IntPtr display, int attr, out IntPtr res);

5
src/Avalonia.OpenGL/Egl/EglPlatformGraphics.cs

@ -1,7 +1,6 @@
using System; using System;
using Avalonia.Logging; using Avalonia.Logging;
using Avalonia.Platform; using Avalonia.Platform;
using static Avalonia.OpenGL.Egl.EglConsts;
namespace Avalonia.OpenGL.Egl namespace Avalonia.OpenGL.Egl
{ {
@ -24,7 +23,7 @@ namespace Avalonia.OpenGL.Egl
AvaloniaLocator.CurrentMutable.Bind<IPlatformGraphics>().ToConstant(feature); AvaloniaLocator.CurrentMutable.Bind<IPlatformGraphics>().ToConstant(feature);
} }
public static EglPlatformGraphics TryCreate() => TryCreate(() => new EglDisplay(new EglDisplayCreationOptions public static EglPlatformGraphics? TryCreate() => TryCreate(() => new EglDisplay(new EglDisplayCreationOptions
{ {
Egl = new EglInterface(), Egl = new EglInterface(),
// Those are expected to be supported by most EGL implementations // Those are expected to be supported by most EGL implementations
@ -32,7 +31,7 @@ namespace Avalonia.OpenGL.Egl
SupportsContextSharing = true SupportsContextSharing = true
})); }));
public static EglPlatformGraphics TryCreate(Func<EglDisplay> displayFactory) public static EglPlatformGraphics? TryCreate(Func<EglDisplay> displayFactory)
{ {
try try
{ {

43
src/Avalonia.OpenGL/Features/ExternalObjectsOpenGlExtensionFeature.cs

@ -63,10 +63,10 @@ public class ExternalObjectsOpenGlExtensionFeature : IGlContextExternalObjectsFe
{ {
private readonly IGlContext _context; private readonly IGlContext _context;
private readonly ExternalObjectsInterface _ext; private readonly ExternalObjectsInterface _ext;
private List<string> _imageTypes = new(); private readonly List<string> _imageTypes = new();
private List<string> _semaphoreTypes = new(); private readonly List<string> _semaphoreTypes = new();
public static ExternalObjectsOpenGlExtensionFeature TryCreate(IGlContext context) public static ExternalObjectsOpenGlExtensionFeature? TryCreate(IGlContext context)
{ {
var extensions = context.GlInterface.GetExtensions(); var extensions = context.GlInterface.GetExtensions();
if (extensions.Contains("GL_EXT_memory_object") && extensions.Contains("GL_EXT_semaphore")) if (extensions.Contains("GL_EXT_memory_object") && extensions.Contains("GL_EXT_semaphore"))
@ -142,10 +142,15 @@ public class ExternalObjectsOpenGlExtensionFeature : IGlContextExternalObjectsFe
public IGlExternalImageTexture ImportImage(IPlatformHandle handle, PlatformGraphicsExternalImageProperties properties) public IGlExternalImageTexture ImportImage(IPlatformHandle handle, PlatformGraphicsExternalImageProperties properties)
{ {
if(!_imageTypes.Contains(handle.HandleDescriptor)) var handleDescriptor = handle.HandleDescriptor;
throw new ArgumentException(handle.HandleDescriptor + " is not supported");
if (string.IsNullOrEmpty(handleDescriptor))
if (handle.HandleDescriptor == KnownPlatformGraphicsExternalImageHandleTypes.VulkanOpaquePosixFileDescriptor) throw new ArgumentException("The handle must have a descriptor", nameof(handle));
if (!_imageTypes.Contains(handleDescriptor))
throw new ArgumentException(handleDescriptor + " is not supported", nameof(handle));
if (handleDescriptor == KnownPlatformGraphicsExternalImageHandleTypes.VulkanOpaquePosixFileDescriptor)
{ {
while (_context.GlInterface.GetError() != 0) while (_context.GlInterface.GetError() != 0)
{ {
@ -174,23 +179,27 @@ public class ExternalObjectsOpenGlExtensionFeature : IGlContextExternalObjectsFe
return new ExternalImageTexture(_context, properties, _ext, memoryObject, texture); return new ExternalImageTexture(_context, properties, _ext, memoryObject, texture);
} }
throw new ArgumentException(handle.HandleDescriptor + " is not supported"); throw new ArgumentException(handleDescriptor + " is not supported", nameof(handle));
} }
public IGlExternalSemaphore ImportSemaphore(IPlatformHandle handle) public IGlExternalSemaphore ImportSemaphore(IPlatformHandle handle)
{ {
if(!_semaphoreTypes.Contains(handle.HandleDescriptor)) var handleDescriptor = handle.HandleDescriptor;
throw new ArgumentException(handle.HandleDescriptor + " is not supported");
if (string.IsNullOrEmpty(handleDescriptor))
throw new ArgumentException("The handle must have a descriptor", nameof(handle));
if (!_semaphoreTypes.Contains(handleDescriptor))
throw new ArgumentException(handleDescriptor + " is not supported");
if (handle.HandleDescriptor == if (handleDescriptor == KnownPlatformGraphicsExternalSemaphoreHandleTypes.VulkanOpaquePosixFileDescriptor)
KnownPlatformGraphicsExternalSemaphoreHandleTypes.VulkanOpaquePosixFileDescriptor)
{ {
_ext.GenSemaphoresEXT(1, out var semaphore); _ext.GenSemaphoresEXT(1, out var semaphore);
_ext.ImportSemaphoreFdEXT(semaphore, GL_HANDLE_TYPE_OPAQUE_FD_EXT, handle.Handle.ToInt32()); _ext.ImportSemaphoreFdEXT(semaphore, GL_HANDLE_TYPE_OPAQUE_FD_EXT, handle.Handle.ToInt32());
return new ExternalSemaphore(_context, _ext, semaphore); return new ExternalSemaphore(_context, _ext, semaphore);
} }
throw new ArgumentException(handle.HandleDescriptor + " is not supported"); throw new ArgumentException(handleDescriptor + " is not supported", nameof(handle));
} }
public CompositionGpuImportedImageSynchronizationCapabilities GetSynchronizationCapabilities(string imageHandleType) public CompositionGpuImportedImageSynchronizationCapabilities GetSynchronizationCapabilities(string imageHandleType)
@ -200,10 +209,10 @@ public class ExternalObjectsOpenGlExtensionFeature : IGlContextExternalObjectsFe
return default; return default;
} }
public byte[] DeviceLuid { get; } public byte[]? DeviceLuid { get; }
public byte[] DeviceUuid { get; } public byte[]? DeviceUuid { get; }
unsafe class ExternalSemaphore : IGlExternalSemaphore private unsafe class ExternalSemaphore : IGlExternalSemaphore
{ {
private readonly IGlContext _context; private readonly IGlContext _context;
private readonly ExternalObjectsInterface _ext; private readonly ExternalObjectsInterface _ext;
@ -242,7 +251,7 @@ public class ExternalObjectsOpenGlExtensionFeature : IGlContextExternalObjectsFe
} }
} }
class ExternalImageTexture : IGlExternalImageTexture private class ExternalImageTexture : IGlExternalImageTexture
{ {
private readonly IGlContext _context; private readonly IGlContext _context;
private readonly ExternalObjectsInterface _ext; private readonly ExternalObjectsInterface _ext;

16
src/Avalonia.OpenGL/GlBasicInfoInterface.cs

@ -2,14 +2,14 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using Avalonia.Platform.Interop;
using Avalonia.SourceGenerator; using Avalonia.SourceGenerator;
namespace Avalonia.OpenGL namespace Avalonia.OpenGL
{ {
public unsafe partial class GlBasicInfoInterface public unsafe partial class GlBasicInfoInterface
{ {
public GlBasicInfoInterface(Func<string, IntPtr> getProcAddress){ public GlBasicInfoInterface(Func<string, IntPtr> getProcAddress)
{
Initialize(getProcAddress); Initialize(getProcAddress);
} }
@ -22,7 +22,7 @@ namespace Avalonia.OpenGL
[GetProcAddress("glGetStringi")] [GetProcAddress("glGetStringi")]
public partial IntPtr GetStringiNative(int v, int v1); public partial IntPtr GetStringiNative(int v, int v1);
public string GetString(int v) public string? GetString(int v)
{ {
var ptr = GetStringNative(v); var ptr = GetStringNative(v);
if (ptr != IntPtr.Zero) if (ptr != IntPtr.Zero)
@ -30,7 +30,7 @@ namespace Avalonia.OpenGL
return null; return null;
} }
public string GetString(int v, int index) public string? GetString(int v, int index)
{ {
var ptr = GetStringiNative(v, index); var ptr = GetStringiNative(v, index);
if (ptr != IntPtr.Zero) if (ptr != IntPtr.Zero)
@ -46,7 +46,13 @@ namespace Avalonia.OpenGL
GetIntegerv(GlConsts.GL_NUM_EXTENSIONS, out int count); GetIntegerv(GlConsts.GL_NUM_EXTENSIONS, out int count);
var rv = new List<string>(count); var rv = new List<string>(count);
for (var c = 0; c < count; c++) for (var c = 0; c < count; c++)
rv.Add(GetString(GlConsts.GL_EXTENSIONS, c)); {
if (GetString(GlConsts.GL_EXTENSIONS, c) is { } extension)
{
rv.Add(extension);
}
}
return rv; return rv;
} }
} }

21
src/Avalonia.OpenGL/GlInterface.cs

@ -11,9 +11,9 @@ namespace Avalonia.OpenGL
public unsafe partial class GlInterface : GlBasicInfoInterface public unsafe partial class GlInterface : GlBasicInfoInterface
{ {
private readonly Func<string, IntPtr> _getProcAddress; private readonly Func<string, IntPtr> _getProcAddress;
public string Version { get; } public string? Version { get; }
public string Vendor { get; } public string? Vendor { get; }
public string Renderer { get; } public string? Renderer { get; }
public GlContextInfo ContextInfo { get; } public GlContextInfo ContextInfo { get; }
public class GlContextInfo public class GlContextInfo
@ -39,9 +39,9 @@ namespace Avalonia.OpenGL
{ {
_getProcAddress = getProcAddress; _getProcAddress = getProcAddress;
ContextInfo = info; ContextInfo = info;
Version = GetString(GlConsts.GL_VERSION); Version = GetString(GL_VERSION);
Renderer = GetString(GlConsts.GL_RENDERER); Renderer = GetString(GL_RENDERER);
Vendor = GetString(GlConsts.GL_VENDOR); Vendor = GetString(GL_VENDOR);
Initialize(getProcAddress, ContextInfo); Initialize(getProcAddress, ContextInfo);
} }
@ -73,9 +73,6 @@ namespace Avalonia.OpenGL
[GetProcAddress("glFinish")] [GetProcAddress("glFinish")]
public partial void Finish(); public partial void Finish();
[GetProcAddress("glGetIntegerv")]
public partial void GetIntegerv(int name, out int rv);
[GetProcAddress("glGenFramebuffers")] [GetProcAddress("glGenFramebuffers")]
public partial void GenFramebuffers(int count, int* res); public partial void GenFramebuffers(int count, int* res);
@ -203,7 +200,7 @@ namespace Avalonia.OpenGL
[GetProcAddress("glGetShaderInfoLog")] [GetProcAddress("glGetShaderInfoLog")]
public partial void GetShaderInfoLog(int shader, int maxLength, out int length, void* infoLog); public partial void GetShaderInfoLog(int shader, int maxLength, out int length, void* infoLog);
public unsafe string CompileShaderAndGetError(int shader, string source) public unsafe string? CompileShaderAndGetError(int shader, string source)
{ {
ShaderSourceString(shader, source); ShaderSourceString(shader, source);
CompileShader(shader); CompileShader(shader);
@ -238,7 +235,7 @@ namespace Avalonia.OpenGL
[GetProcAddress("glGetProgramInfoLog")] [GetProcAddress("glGetProgramInfoLog")]
public partial void GetProgramInfoLog(int program, int maxLength, out int len, void* infoLog); public partial void GetProgramInfoLog(int program, int maxLength, out int len, void* infoLog);
public unsafe string LinkProgramAndGetError(int program) public unsafe string? LinkProgramAndGetError(int program)
{ {
LinkProgram(program); LinkProgram(program);
int compiled; int compiled;
@ -374,4 +371,4 @@ namespace Avalonia.OpenGL
}); });
} }
} }
} }

3
src/Avalonia.OpenGL/IGlContext.cs

@ -12,10 +12,9 @@ namespace Avalonia.OpenGL
int SampleCount { get; } int SampleCount { get; }
int StencilSize { get; } int StencilSize { get; }
IDisposable MakeCurrent(); IDisposable MakeCurrent();
IDisposable EnsureCurrent();
bool IsSharedWith(IGlContext context); bool IsSharedWith(IGlContext context);
bool CanCreateSharedContext { get; } bool CanCreateSharedContext { get; }
IGlContext CreateSharedContext(IEnumerable<GlVersion> preferredVersions = null); IGlContext? CreateSharedContext(IEnumerable<GlVersion>? preferredVersions = null);
} }
public interface IGlPlatformSurfaceRenderTargetFactory public interface IGlPlatformSurfaceRenderTargetFactory

3
src/Avalonia.OpenGL/IOpenGlTextureSharingRenderInterfaceContextFeature.cs

@ -1,4 +1,3 @@
using System;
using System.Collections.Generic; using System.Collections.Generic;
using Avalonia.Rendering.Composition; using Avalonia.Rendering.Composition;
@ -7,7 +6,7 @@ namespace Avalonia.OpenGL
public interface IOpenGlTextureSharingRenderInterfaceContextFeature public interface IOpenGlTextureSharingRenderInterfaceContextFeature
{ {
bool CanCreateSharedContext { get; } bool CanCreateSharedContext { get; }
IGlContext CreateSharedContext(IEnumerable<GlVersion> preferredVersions = null); IGlContext? CreateSharedContext(IEnumerable<GlVersion>? preferredVersions = null);
ICompositionImportableOpenGlSharedTexture CreateSharedTextureForComposition(IGlContext context, PixelSize size); ICompositionImportableOpenGlSharedTexture CreateSharedTextureForComposition(IGlContext context, PixelSize size);
} }

6
src/Avalonia.OpenGL/OpenGlException.cs

@ -5,13 +5,13 @@ namespace Avalonia.OpenGL
{ {
public class OpenGlException : Exception public class OpenGlException : Exception
{ {
public int? ErrorCode { get; private set; } public int? ErrorCode { get; }
public OpenGlException(string message) : base(message) public OpenGlException(string? message) : base(message)
{ {
} }
private OpenGlException(string message, int errorCode) : base(message) private OpenGlException(string? message, int errorCode) : base(message)
{ {
ErrorCode = errorCode; ErrorCode = errorCode;
} }

12
src/Avalonia.X11/NativeDialogs/CompositeStorageProvider.cs

@ -62,21 +62,21 @@ internal class CompositeStorageProvider : IStorageProvider
return await provider.OpenFolderBookmarkAsync(bookmark).ConfigureAwait(false); return await provider.OpenFolderBookmarkAsync(bookmark).ConfigureAwait(false);
} }
public async Task<IStorageFile?> TryGetFileFromPath(Uri filePath) public async Task<IStorageFile?> TryGetFileFromPathAsync(Uri filePath)
{ {
var provider = await EnsureStorageProvider().ConfigureAwait(false); var provider = await EnsureStorageProvider().ConfigureAwait(false);
return await provider.TryGetFileFromPath(filePath).ConfigureAwait(false); return await provider.TryGetFileFromPathAsync(filePath).ConfigureAwait(false);
} }
public async Task<IStorageFolder?> TryGetFolderFromPath(Uri folderPath) public async Task<IStorageFolder?> TryGetFolderFromPathAsync(Uri folderPath)
{ {
var provider = await EnsureStorageProvider().ConfigureAwait(false); var provider = await EnsureStorageProvider().ConfigureAwait(false);
return await provider.TryGetFolderFromPath(folderPath).ConfigureAwait(false); return await provider.TryGetFolderFromPathAsync(folderPath).ConfigureAwait(false);
} }
public async Task<IStorageFolder?> TryGetWellKnownFolder(WellKnownFolder wellKnownFolder) public async Task<IStorageFolder?> TryGetWellKnownFolderAsync(WellKnownFolder wellKnownFolder)
{ {
var provider = await EnsureStorageProvider().ConfigureAwait(false); var provider = await EnsureStorageProvider().ConfigureAwait(false);
return await provider.TryGetWellKnownFolder(wellKnownFolder).ConfigureAwait(false); return await provider.TryGetWellKnownFolderAsync(wellKnownFolder).ConfigureAwait(false);
} }
} }

2
src/Avalonia.X11/NativeDialogs/GtkNativeFileDialogs.cs

@ -196,7 +196,7 @@ namespace Avalonia.X11.NativeDialogs
gtk_dialog_add_button(dlg, open, GtkResponseType.Cancel); gtk_dialog_add_button(dlg, open, GtkResponseType.Cancel);
} }
var folderLocalPath = initialFolder?.TryGetFullPath(); var folderLocalPath = initialFolder?.TryGetLocalPath();
if (folderLocalPath is not null) if (folderLocalPath is not null)
{ {
using var dir = new Utf8Buffer(folderLocalPath); using var dir = new Utf8Buffer(folderLocalPath);

14
src/Browser/Avalonia.Browser.Blazor/AvaloniaView.cs

@ -30,12 +30,10 @@ public class AvaloniaView : ComponentBase
builder.CloseElement(); builder.CloseElement();
} }
protected override async Task OnInitializedAsync() protected override void OnAfterRender(bool firstRender)
{ {
if (OperatingSystem.IsBrowser()) if (firstRender)
{ {
await AvaloniaModule.ImportMain();
_browserView = new Browser.AvaloniaView(_containerId); _browserView = new Browser.AvaloniaView(_containerId);
if (Application.Current?.ApplicationLifetime is ISingleViewApplicationLifetime lifetime) if (Application.Current?.ApplicationLifetime is ISingleViewApplicationLifetime lifetime)
{ {
@ -43,4 +41,12 @@ public class AvaloniaView : ComponentBase
} }
} }
} }
protected override void OnInitialized()
{
if (!OperatingSystem.IsBrowser())
{
throw new NotSupportedException("Avalonia doesn't support server-side Blazor");
}
}
} }

37
src/Browser/Avalonia.Browser.Blazor/BlazorSingleViewLifetime.cs

@ -1,33 +1,28 @@
using System.Runtime.Versioning; using System;
using System.Runtime.Versioning;
using System.Threading.Tasks;
using Avalonia;
using Avalonia.Browser.Interop;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Controls.ApplicationLifetimes;
namespace Avalonia.Browser.Blazor; namespace Avalonia.Browser.Blazor;
public static class WebAppBuilder public static class BlazorAppBuilder
{ {
public static AppBuilder SetupWithSingleViewLifetime( /// <summary>
this AppBuilder builder) /// Configures blazor backend, loads avalonia javascript modules and creates a single view lifetime.
/// </summary>
/// <param name="builder">Application builder.</param>
/// <param name="options">Browser backend specific options.</param>
public static async Task StartBlazorAppAsync(this AppBuilder builder, BrowserPlatformOptions? options = null)
{ {
return builder.SetupWithLifetime(new BlazorSingleViewLifetime()); options ??= new BrowserPlatformOptions();
} options.FrameworkAssetPathResolver ??= filePath => $"/_content/Avalonia.Browser.Blazor/{filePath}";
public static AppBuilder UseBlazor(this AppBuilder builder) builder = await BrowserAppBuilder.PreSetupBrowser(builder, options);
{
return builder
.UseBrowser()
.With(new BrowserPlatformOptions
{
FrameworkAssetPathResolver = new(filePath => $"/_content/Avalonia.Browser.Blazor/{filePath}")
});
}
public static AppBuilder Configure<TApp>() builder.SetupWithLifetime(new BlazorSingleViewLifetime());
where TApp : Application, new()
{
return AppBuilder.Configure<TApp>()
.UseBlazor();
} }
internal class BlazorSingleViewLifetime : ISingleViewApplicationLifetime internal class BlazorSingleViewLifetime : ISingleViewApplicationLifetime

10
src/Browser/Avalonia.Browser/AvaloniaView.cs

@ -20,7 +20,7 @@ using static System.Runtime.CompilerServices.RuntimeHelpers;
namespace Avalonia.Browser namespace Avalonia.Browser
{ {
public partial class AvaloniaView : ITextInputMethodImpl public class AvaloniaView : ITextInputMethodImpl
{ {
private static readonly PooledList<RawPointerPoint> s_intermediatePointsPooledList = new(ClearMode.Never); private static readonly PooledList<RawPointerPoint> s_intermediatePointsPooledList = new(ClearMode.Never);
private readonly BrowserTopLevelImpl _topLevelImpl; private readonly BrowserTopLevelImpl _topLevelImpl;
@ -43,8 +43,9 @@ namespace Avalonia.Browser
private bool _useGL; private bool _useGL;
private ITextInputMethodClient? _client; private ITextInputMethodClient? _client;
/// <param name="divId">ID of the html element where avalonia content should be rendered.</param>
public AvaloniaView(string divId) public AvaloniaView(string divId)
: this(DomHelper.GetElementById(divId) ?? throw new Exception($"Element with id {divId} was not found in the html document.")) : this(DomHelper.GetElementById(divId) ?? throw new Exception($"Element with id '{divId}' was not found in the html document."))
{ {
} }
@ -380,12 +381,10 @@ namespace Avalonia.Browser
{ {
if (_useGL && (_jsGlInfo == null)) if (_useGL && (_jsGlInfo == null))
{ {
Console.WriteLine("nothing to render");
return; return;
} }
if (_canvasSize.Width <= 0 || _canvasSize.Height <= 0 || _dpi <= 0) if (_canvasSize.Width <= 0 || _canvasSize.Height <= 0 || _dpi <= 0)
{ {
Console.WriteLine("nothing to render");
return; return;
} }
@ -458,7 +457,6 @@ namespace Avalonia.Browser
void ITextInputMethodImpl.SetClient(ITextInputMethodClient? client) void ITextInputMethodImpl.SetClient(ITextInputMethodClient? client)
{ {
Console.WriteLine("Set Client");
if (_client != null) if (_client != null)
{ {
_client.SurroundingTextChanged -= SurroundingTextChanged; _client.SurroundingTextChanged -= SurroundingTextChanged;
@ -481,8 +479,6 @@ namespace Avalonia.Browser
var surroundingText = _client.SurroundingText; var surroundingText = _client.SurroundingText;
InputHelper.SetSurroundingText(_inputElement, surroundingText.Text, surroundingText.AnchorOffset, surroundingText.CursorOffset); InputHelper.SetSurroundingText(_inputElement, surroundingText.Text, surroundingText.AnchorOffset, surroundingText.CursorOffset);
Console.WriteLine("Shown, focused and surrounded.");
} }
else else
{ {

83
src/Browser/Avalonia.Browser/BrowserAppBuilder.cs

@ -0,0 +1,83 @@
using System;
using System.Threading.Tasks;
using Avalonia.Browser.Interop;
namespace Avalonia.Browser;
public class BrowserPlatformOptions
{
/// <summary>
/// Defines paths where avalonia modules and service locator should be resolved.
/// If null, default path resolved depending on the backend (browser or blazor) is used.
/// </summary>
public Func<string, string>? FrameworkAssetPathResolver { get; set; }
}
public static class BrowserAppBuilder
{
/// <summary>
/// Configures browser backend, loads avalonia javascript modules and creates a single view lifetime from the passed <see cref="mainDivId"/> parameter.
/// </summary>
/// <param name="builder">Application builder.</param>
/// <param name="mainDivId">ID of the html element where avalonia content should be rendered.</param>
/// <param name="options">Browser backend specific options.</param>
public static async Task StartBrowserAppAsync(this AppBuilder builder, string mainDivId, BrowserPlatformOptions? options = null)
{
if (mainDivId is null)
{
throw new ArgumentNullException(nameof(mainDivId));
}
builder = await PreSetupBrowser(builder, options);
var lifetime = new BrowserSingleViewLifetime();
builder
.AfterSetup(_ =>
{
lifetime.View = new AvaloniaView(mainDivId);
})
.SetupWithLifetime(lifetime);
}
/// <summary>
/// Loads avalonia javascript modules and configures browser backend.
/// </summary>
/// <param name="builder">Application builder.</param>
/// <param name="options">Browser backend specific options.</param>
/// <remarks>
/// This method doesn't creates any avalonia views to be rendered. To do so create an <see cref="AvaloniaView"/> object.
/// Alternatively, you can call <see cref="StartBrowserAppAsync"/> method instead of <see cref="SetupBrowserAppAsync"/>.
/// </remarks>
public static async Task SetupBrowserAppAsync(this AppBuilder builder, BrowserPlatformOptions? options = null)
{
builder = await PreSetupBrowser(builder, options);
builder
.SetupWithoutStarting();
}
internal static async Task<AppBuilder> PreSetupBrowser(AppBuilder builder, BrowserPlatformOptions? options)
{
options ??= new BrowserPlatformOptions();
options.FrameworkAssetPathResolver ??= fileName => $"./{fileName}";
AvaloniaLocator.CurrentMutable.Bind<BrowserPlatformOptions>().ToConstant(options);
await AvaloniaModule.ImportMain();
if (builder.WindowingSubsystemInitializer is null)
{
builder = builder.UseBrowser();
}
return builder;
}
public static AppBuilder UseBrowser(
this AppBuilder builder)
{
return builder
.UseWindowingSubsystem(BrowserWindowingPlatform.Register)
.UseSkia();
}
}

40
src/Browser/Avalonia.Browser/BrowserPlatformSettings.cs

@ -1,4 +1,5 @@
using Avalonia.Browser.Interop; using System;
using Avalonia.Browser.Interop;
using Avalonia.Platform; using Avalonia.Platform;
namespace Avalonia.Browser; namespace Avalonia.Browser;
@ -7,25 +8,44 @@ internal class BrowserPlatformSettings : DefaultPlatformSettings
{ {
private bool _isDarkMode; private bool _isDarkMode;
private bool _isHighContrast; private bool _isHighContrast;
private bool _isInitialized;
public BrowserPlatformSettings()
public override event EventHandler<PlatformColorValues>? ColorValuesChanged
{ {
var obj = DomHelper.ObserveDarkMode((isDarkMode, isHighContrast) => add
{ {
_isDarkMode = isDarkMode; EnsureBackend();
_isHighContrast = isHighContrast; base.ColorValuesChanged += value;
OnColorValuesChanged(GetColorValues()); }
}); remove => base.ColorValuesChanged -= value;
_isDarkMode = obj.GetPropertyAsBoolean("isDarkMode");
_isHighContrast = obj.GetPropertyAsBoolean("isHighContrast");
} }
public override PlatformColorValues GetColorValues() public override PlatformColorValues GetColorValues()
{ {
EnsureBackend();
return base.GetColorValues() with return base.GetColorValues() with
{ {
ThemeVariant = _isDarkMode ? PlatformThemeVariant.Dark : PlatformThemeVariant.Light, ThemeVariant = _isDarkMode ? PlatformThemeVariant.Dark : PlatformThemeVariant.Light,
ContrastPreference = _isHighContrast ? ColorContrastPreference.High : ColorContrastPreference.NoPreference ContrastPreference = _isHighContrast ? ColorContrastPreference.High : ColorContrastPreference.NoPreference
}; };
} }
private void EnsureBackend()
{
if (!_isInitialized)
{
// WASM module has async nature of initialization. We can't native code right away during components registration.
_isInitialized = true;
var obj = DomHelper.ObserveDarkMode((isDarkMode, isHighContrast) =>
{
_isDarkMode = isDarkMode;
_isHighContrast = isHighContrast;
OnColorValuesChanged(GetColorValues());
});
_isDarkMode = obj.GetPropertyAsBoolean("isDarkMode");
_isHighContrast = obj.GetPropertyAsBoolean("isHighContrast");
}
}
} }

51
src/Browser/Avalonia.Browser/BrowserSingleViewLifetime.cs

@ -1,47 +1,36 @@
using System; using System;
using System.Diagnostics.CodeAnalysis;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Controls.ApplicationLifetimes;
using System.Runtime.Versioning; using System.Runtime.Versioning;
using Avalonia.Browser;
namespace Avalonia.Browser; namespace Avalonia;
public class BrowserSingleViewLifetime : ISingleViewApplicationLifetime internal class BrowserSingleViewLifetime : ISingleViewApplicationLifetime
{ {
public AvaloniaView? View; public AvaloniaView? View;
public Control? MainView public Control? MainView
{ {
get => View!.Content; get
set => View!.Content = value; {
} EnsureView();
} return View.Content;
}
public class BrowserPlatformOptions set
{ {
public Func<string, string> FrameworkAssetPathResolver { get; set; } = new(fileName => $"./{fileName}"); EnsureView();
} View.Content = value;
}
public static class WebAppBuilder
{
public static AppBuilder SetupBrowserApp(
this AppBuilder builder, string mainDivId)
{
var lifetime = new BrowserSingleViewLifetime();
return builder
.UseBrowser()
.AfterSetup(b =>
{
lifetime.View = new AvaloniaView(mainDivId);
})
.SetupWithLifetime(lifetime);
} }
public static AppBuilder UseBrowser( [MemberNotNull(nameof(View))]
this AppBuilder builder) private void EnsureView()
{ {
return builder if (View is null)
.UseWindowingSubsystem(BrowserWindowingPlatform.Register) {
.UseSkia(); throw new InvalidOperationException("Browser lifetime was not initialized. Make sure AppBuilder.StartBrowserApp was called.");
}
} }
} }

4
src/Browser/Avalonia.Browser/Interop/AvaloniaModule.cs

@ -11,13 +11,13 @@ internal static partial class AvaloniaModule
public static Task ImportMain() public static Task ImportMain()
{ {
var options = AvaloniaLocator.Current.GetService<BrowserPlatformOptions>() ?? new BrowserPlatformOptions(); var options = AvaloniaLocator.Current.GetService<BrowserPlatformOptions>() ?? new BrowserPlatformOptions();
return JSHost.ImportAsync(MainModuleName, options.FrameworkAssetPathResolver("avalonia.js")); return JSHost.ImportAsync(MainModuleName, options.FrameworkAssetPathResolver!("avalonia.js"));
} }
public static Task ImportStorage() public static Task ImportStorage()
{ {
var options = AvaloniaLocator.Current.GetService<BrowserPlatformOptions>() ?? new BrowserPlatformOptions(); var options = AvaloniaLocator.Current.GetService<BrowserPlatformOptions>() ?? new BrowserPlatformOptions();
return JSHost.ImportAsync(StorageModuleName, options.FrameworkAssetPathResolver("storage.js")); return JSHost.ImportAsync(StorageModuleName, options.FrameworkAssetPathResolver!("storage.js"));
} }
[JSImport("Caniuse.isMobile", AvaloniaModule.MainModuleName)] [JSImport("Caniuse.isMobile", AvaloniaModule.MainModuleName)]

12
src/Browser/Avalonia.Browser/Interop/StorageHelper.cs

@ -5,14 +5,8 @@ namespace Avalonia.Browser.Interop;
internal static partial class StorageHelper internal static partial class StorageHelper
{ {
[JSImport("Caniuse.canShowOpenFilePicker", AvaloniaModule.MainModuleName)] [JSImport("Caniuse.hasNativeFilePicker", AvaloniaModule.MainModuleName)]
public static partial bool CanShowOpenFilePicker(); public static partial bool HasNativeFilePicker();
[JSImport("Caniuse.canShowSaveFilePicker", AvaloniaModule.MainModuleName)]
public static partial bool CanShowSaveFilePicker();
[JSImport("Caniuse.canShowDirectoryPicker", AvaloniaModule.MainModuleName)]
public static partial bool CanShowDirectoryPicker();
[JSImport("StorageProvider.selectFolderDialog", AvaloniaModule.StorageModuleName)] [JSImport("StorageProvider.selectFolderDialog", AvaloniaModule.StorageModuleName)]
public static partial Task<JSObject?> SelectFolderDialog(JSObject? startIn); public static partial Task<JSObject?> SelectFolderDialog(JSObject? startIn);
@ -54,5 +48,5 @@ internal static partial class StorageHelper
public static partial JSObject[] ItemsArray(JSObject item); public static partial JSObject[] ItemsArray(JSObject item);
[JSImport("StorageProvider.createAcceptType", AvaloniaModule.StorageModuleName)] [JSImport("StorageProvider.createAcceptType", AvaloniaModule.StorageModuleName)]
public static partial JSObject CreateAcceptType(string description, string[] mimeTypes); public static partial JSObject CreateAcceptType(string description, string[] mimeTypes, string[]? extensions);
} }

28
src/Browser/Avalonia.Browser/Storage/BrowserStorageProvider.cs

@ -20,9 +20,9 @@ internal class BrowserStorageProvider : IStorageProvider
private readonly Lazy<Task> _lazyModule = new(() => AvaloniaModule.ImportStorage()); private readonly Lazy<Task> _lazyModule = new(() => AvaloniaModule.ImportStorage());
public bool CanOpen => StorageHelper.CanShowOpenFilePicker(); public bool CanOpen => true;
public bool CanSave => StorageHelper.CanShowSaveFilePicker(); public bool CanSave => StorageHelper.HasNativeFilePicker();
public bool CanPickFolder => StorageHelper.CanShowDirectoryPicker(); public bool CanPickFolder => true;
public async Task<IReadOnlyList<IStorageFile>> OpenFilePickerAsync(FilePickerOpenOptions options) public async Task<IReadOnlyList<IStorageFile>> OpenFilePickerAsync(FilePickerOpenOptions options)
{ {
@ -116,17 +116,17 @@ internal class BrowserStorageProvider : IStorageProvider
return item is not null ? new JSStorageFolder(item) : null; return item is not null ? new JSStorageFolder(item) : null;
} }
public Task<IStorageFile?> TryGetFileFromPath(Uri filePath) public Task<IStorageFile?> TryGetFileFromPathAsync(Uri filePath)
{ {
return Task.FromResult<IStorageFile?>(null); return Task.FromResult<IStorageFile?>(null);
} }
public Task<IStorageFolder?> TryGetFolderFromPath(Uri folderPath) public Task<IStorageFolder?> TryGetFolderFromPathAsync(Uri folderPath)
{ {
return Task.FromResult<IStorageFolder?>(null); return Task.FromResult<IStorageFolder?>(null);
} }
public async Task<IStorageFolder?> TryGetWellKnownFolder(WellKnownFolder wellKnownFolder) public async Task<IStorageFolder?> TryGetWellKnownFolderAsync(WellKnownFolder wellKnownFolder)
{ {
await _lazyModule.Value; await _lazyModule.Value;
var directory = StorageHelper.CreateWellKnownDirectory(wellKnownFolder switch var directory = StorageHelper.CreateWellKnownDirectory(wellKnownFolder switch
@ -147,7 +147,7 @@ internal class BrowserStorageProvider : IStorageProvider
{ {
var types = input? var types = input?
.Where(t => t.MimeTypes?.Any() == true && t != FilePickerFileTypes.All) .Where(t => t.MimeTypes?.Any() == true && t != FilePickerFileTypes.All)
.Select(t => StorageHelper.CreateAcceptType(t.Name, t.MimeTypes!.ToArray())) .Select(t => StorageHelper.CreateAcceptType(t.Name, t.MimeTypes!.ToArray(), t.TryGetExtensions()?.ToArray()))
.ToArray(); .ToArray();
if (types?.Length == 0) if (types?.Length == 0)
{ {
@ -186,10 +186,15 @@ internal abstract class JSStorageItem : IStorageBookmarkItem
dateModified: lastModified > 0 ? DateTimeOffset.FromUnixTimeMilliseconds(lastModified.Value) : null); dateModified: lastModified > 0 ? DateTimeOffset.FromUnixTimeMilliseconds(lastModified.Value) : null);
} }
public bool CanBookmark => true; public bool CanBookmark => StorageHelper.HasNativeFilePicker();
public Task<string?> SaveBookmarkAsync() public Task<string?> SaveBookmarkAsync()
{ {
if (!CanBookmark)
{
return Task.FromResult<string?>(null);
}
return StorageHelper.SaveBookmark(FileHandle); return StorageHelper.SaveBookmark(FileHandle);
} }
@ -200,6 +205,11 @@ internal abstract class JSStorageItem : IStorageBookmarkItem
public Task ReleaseBookmarkAsync() public Task ReleaseBookmarkAsync()
{ {
if (!CanBookmark)
{
return Task.CompletedTask;
}
return StorageHelper.DeleteBookmark(FileHandle); return StorageHelper.DeleteBookmark(FileHandle);
} }
@ -216,7 +226,6 @@ internal class JSStorageFile : JSStorageItem, IStorageBookmarkFile
{ {
} }
public bool CanOpenRead => true;
public async Task<Stream> OpenReadAsync() public async Task<Stream> OpenReadAsync()
{ {
try try
@ -230,7 +239,6 @@ internal class JSStorageFile : JSStorageItem, IStorageBookmarkFile
} }
} }
public bool CanOpenWrite => true;
public async Task<Stream> OpenWriteAsync() public async Task<Stream> OpenWriteAsync()
{ {
try try

2
src/Browser/Avalonia.Browser/webapp/.eslintrc.json

@ -43,5 +43,5 @@
} }
] ]
}, },
"ignorePatterns": ["types/*"] "ignorePatterns": ["types/*","node_modules/*"]
} }

2
src/Browser/Avalonia.Browser/webapp/build.js

@ -7,7 +7,7 @@ require("esbuild").build({
bundle: true, bundle: true,
minify: true, minify: true,
format: "esm", format: "esm",
target: "es2016", target: "es2018",
platform: "browser", platform: "browser",
sourcemap: "linked", sourcemap: "linked",
loader: { ".ts": "ts" } loader: { ".ts": "ts" }

18
src/Browser/Avalonia.Browser/webapp/modules/avalonia.ts

@ -1,4 +1,3 @@
import { RuntimeAPI } from "../types/dotnet";
import { SizeWatcher, DpiWatcher, Canvas } from "./avalonia/canvas"; import { SizeWatcher, DpiWatcher, Canvas } from "./avalonia/canvas";
import { InputHelper } from "./avalonia/input"; import { InputHelper } from "./avalonia/input";
import { AvaloniaDOM } from "./avalonia/dom"; import { AvaloniaDOM } from "./avalonia/dom";
@ -7,19 +6,6 @@ import { StreamHelper } from "./avalonia/stream";
import { NativeControlHost } from "./avalonia/nativeControlHost"; import { NativeControlHost } from "./avalonia/nativeControlHost";
import { NavigationHelper } from "./avalonia/navigationHelper"; import { NavigationHelper } from "./avalonia/navigationHelper";
async function registerAvaloniaModule(api: RuntimeAPI): Promise<void> {
api.setModuleImports("avalonia", {
Caniuse,
Canvas,
InputHelper,
SizeWatcher,
DpiWatcher,
AvaloniaDOM,
StreamHelper,
NativeControlHost,
NavigationHelper
});
}
export { export {
Caniuse, Caniuse,
Canvas, Canvas,
@ -29,7 +15,5 @@ export {
AvaloniaDOM, AvaloniaDOM,
StreamHelper, StreamHelper,
NativeControlHost, NativeControlHost,
NavigationHelper, NavigationHelper
registerAvaloniaModule
}; };

12
src/Browser/Avalonia.Browser/webapp/modules/avalonia/caniuse.ts

@ -1,14 +1,6 @@
export class Caniuse { export class Caniuse {
public static canShowOpenFilePicker(): boolean { public static hasNativeFilePicker(): boolean {
return typeof globalThis.showOpenFilePicker !== "undefined"; return "showSaveFilePicker" in globalThis;
}
public static canShowSaveFilePicker(): boolean {
return typeof globalThis.showSaveFilePicker !== "undefined";
}
public static canShowDirectoryPicker(): boolean {
return typeof globalThis.showDirectoryPicker !== "undefined";
} }
public static isMobile(): boolean { public static isMobile(): boolean {

3
src/Browser/Avalonia.Browser/webapp/modules/avalonia/stream.ts

@ -1,3 +1,4 @@
import FileSystemWritableFileStream from "native-file-system-adapter/types/src/FileSystemWritableFileStream";
import { IMemoryView } from "../../types/dotnet"; import { IMemoryView } from "../../types/dotnet";
export class StreamHelper { export class StreamHelper {
@ -17,7 +18,7 @@ export class StreamHelper {
const array = new Uint8Array(span.byteLength); const array = new Uint8Array(span.byteLength);
span.copyTo(array); span.copyTo(array);
const data: WriteParams = { const data = {
type: "write", type: "write",
data: array data: array
}; };

50
src/Browser/Avalonia.Browser/webapp/modules/storage/storageItem.ts

@ -1,8 +1,10 @@
import { avaloniaDb, fileBookmarksStore } from "./indexedDb"; import { avaloniaDb, fileBookmarksStore } from "./indexedDb";
import { FileSystemFileHandle, FileSystemDirectoryHandle, FileSystemWritableFileStream } from "native-file-system-adapter";
import { Caniuse } from "../avalonia";
export class StorageItem { export class StorageItem {
constructor( constructor(
public handle?: FileSystemHandle, public handle?: FileSystemFileHandle | FileSystemDirectoryHandle,
private readonly bookmarkId?: string, private readonly bookmarkId?: string,
public wellKnownType?: WellKnownDirectory public wellKnownType?: WellKnownDirectory
) { ) {
@ -27,39 +29,44 @@ export class StorageItem {
} }
public static async openRead(item: StorageItem): Promise<Blob> { public static async openRead(item: StorageItem): Promise<Blob> {
if (!(item.handle instanceof FileSystemFileHandle)) { if (!item.handle || item.kind !== "file") {
throw new Error("StorageItem is not a file"); throw new Error("StorageItem is not a file");
} }
await item.verityPermissions("read"); await item.verityPermissions("read");
const file = await item.handle.getFile(); const file = await (item.handle as FileSystemFileHandle).getFile();
return file; return file;
} }
public static async openWrite(item: StorageItem): Promise<FileSystemWritableFileStream> { public static async openWrite(item: StorageItem): Promise<FileSystemWritableFileStream> {
if (!(item.handle instanceof FileSystemFileHandle)) { if (!item.handle || item.kind !== "file") {
throw new Error("StorageItem is not a file"); throw new Error("StorageItem is not a file");
} }
await item.verityPermissions("readwrite"); await item.verityPermissions("readwrite");
return await item.handle.createWritable({ keepExistingData: true }); return await (item.handle as FileSystemFileHandle).createWritable({ keepExistingData: true });
} }
public static async getProperties(item: StorageItem): Promise<{ Size: number; LastModified: number; Type: string } | null> { public static async getProperties(item: StorageItem): Promise<{ Size: number; LastModified: number; Type: string } | null> {
const file = item.handle instanceof FileSystemFileHandle && // getFile can fail with an exception depending if we use polyfill with a save file dialog or not.
await item.handle.getFile(); try {
const file = item.handle instanceof FileSystemFileHandle &&
if (!file) { await item.handle.getFile();
if (!file) {
return null;
}
return {
Size: file.size,
LastModified: file.lastModified,
Type: file.type
};
} catch {
return null; return null;
} }
return {
Size: file.size,
LastModified: file.lastModified,
Type: file.type
};
} }
public static async getItems(item: StorageItem): Promise<StorageItems> { public static async getItems(item: StorageItem): Promise<StorageItems> {
@ -74,11 +81,16 @@ export class StorageItem {
return new StorageItems(items); return new StorageItems(items);
} }
private async verityPermissions(mode: FileSystemPermissionMode): Promise<void | never> { private async verityPermissions(mode: "read" | "readwrite"): Promise<void | never> {
if (!this.handle) { if (!this.handle) {
return; return;
} }
// If we are using polyfill, let it decide permissions by itself, we can't request anything in this case.
if (!Caniuse.hasNativeFilePicker()) {
return;
}
if (await this.handle.queryPermission({ mode }) === "granted") { if (await this.handle.queryPermission({ mode }) === "granted") {
return; return;
} }
@ -93,7 +105,9 @@ export class StorageItem {
if (item.bookmarkId) { if (item.bookmarkId) {
return item.bookmarkId; return item.bookmarkId;
} }
if (!item.handle) {
// Bookmarks are not supported with polyfill.
if (!item.handle || !Caniuse.hasNativeFilePicker()) {
return null; return null;
} }
@ -107,7 +121,7 @@ export class StorageItem {
} }
public static async deleteBookmark(item: StorageItem): Promise<void> { public static async deleteBookmark(item: StorageItem): Promise<void> {
if (!item.bookmarkId) { if (!item.bookmarkId || !Caniuse.hasNativeFilePicker()) {
return; return;
} }

29
src/Browser/Avalonia.Browser/webapp/modules/storage/storageProvider.ts

@ -1,14 +1,12 @@
import { avaloniaDb, fileBookmarksStore } from "./indexedDb"; import { avaloniaDb, fileBookmarksStore } from "./indexedDb";
import { StorageItem, StorageItems } from "./storageItem"; import { StorageItem, StorageItems } from "./storageItem";
import { showOpenFilePicker, showDirectoryPicker, FileSystemFileHandle } from "native-file-system-adapter";
declare global { declare global {
type WellKnownDirectory = "desktop" | "documents" | "downloads" | "music" | "pictures" | "videos"; type WellKnownDirectory = "desktop" | "documents" | "downloads" | "music" | "pictures" | "videos";
type StartInDirectory = WellKnownDirectory | FileSystemHandle; interface FilePickerAcceptType {
interface OpenFilePickerOptions { description?: string | undefined;
startIn?: StartInDirectory; accept: Record<string, string | string[]>;
}
interface SaveFilePickerOptions {
startIn?: StartInDirectory;
} }
} }
@ -16,39 +14,40 @@ export class StorageProvider {
public static async selectFolderDialog( public static async selectFolderDialog(
startIn: StorageItem | null): Promise<StorageItem> { startIn: StorageItem | null): Promise<StorageItem> {
// 'Picker' API doesn't accept "null" as a parameter, so it should be set to undefined. // 'Picker' API doesn't accept "null" as a parameter, so it should be set to undefined.
const options: DirectoryPickerOptions = { const options = {
startIn: (startIn?.wellKnownType ?? startIn?.handle ?? undefined) startIn: (startIn?.wellKnownType ?? startIn?.handle ?? undefined)
}; };
const handle = await window.showDirectoryPicker(options); const handle = await showDirectoryPicker(options as any);
return new StorageItem(handle); return new StorageItem(handle);
} }
public static async openFileDialog( public static async openFileDialog(
startIn: StorageItem | null, multiple: boolean, startIn: StorageItem | null, multiple: boolean,
types: FilePickerAcceptType[] | null, excludeAcceptAllOption: boolean): Promise<StorageItems> { types: FilePickerAcceptType[] | null, excludeAcceptAllOption: boolean): Promise<StorageItems> {
const options: OpenFilePickerOptions = { const options = {
startIn: (startIn?.wellKnownType ?? startIn?.handle ?? undefined), startIn: (startIn?.wellKnownType ?? startIn?.handle ?? undefined),
multiple, multiple,
excludeAcceptAllOption, excludeAcceptAllOption,
types: (types ?? undefined) types: (types ?? undefined)
}; };
const handles = await window.showOpenFilePicker(options); const handles = await showOpenFilePicker(options);
return new StorageItems(handles.map((handle: FileSystemHandle) => new StorageItem(handle))); return new StorageItems(handles.map((handle: FileSystemFileHandle) => new StorageItem(handle)));
} }
public static async saveFileDialog( public static async saveFileDialog(
startIn: StorageItem | null, suggestedName: string | null, startIn: StorageItem | null, suggestedName: string | null,
types: FilePickerAcceptType[] | null, excludeAcceptAllOption: boolean): Promise<StorageItem> { types: FilePickerAcceptType[] | null, excludeAcceptAllOption: boolean): Promise<StorageItem> {
const options: SaveFilePickerOptions = { const options = {
startIn: (startIn?.wellKnownType ?? startIn?.handle ?? undefined), startIn: (startIn?.wellKnownType ?? startIn?.handle ?? undefined),
suggestedName: (suggestedName ?? undefined), suggestedName: (suggestedName ?? undefined),
excludeAcceptAllOption, excludeAcceptAllOption,
types: (types ?? undefined) types: (types ?? undefined)
}; };
const handle = await window.showSaveFilePicker(options); // Always prefer native save file picker, as polyfill solutions are not reliable.
const handle = await (globalThis as any).showSaveFilePicker(options);
return new StorageItem(handle); return new StorageItem(handle);
} }
@ -62,9 +61,9 @@ export class StorageProvider {
} }
} }
public static createAcceptType(description: string, mimeTypes: string[]): FilePickerAcceptType { public static createAcceptType(description: string, mimeTypes: string[], extensions: string[] | undefined): FilePickerAcceptType {
const accept: Record<string, string[]> = {}; const accept: Record<string, string[]> = {};
mimeTypes.forEach(a => { accept[a] = []; }); mimeTypes.forEach(a => { accept[a] = extensions ?? []; });
return { description, accept }; return { description, accept };
} }
} }

117
src/Browser/Avalonia.Browser/webapp/package-lock.json

@ -5,9 +5,11 @@
"packages": { "packages": {
"": { "": {
"name": "avalonia.browser", "name": "avalonia.browser",
"dependencies": {
"native-file-system-adapter": "github:jimmywarting/native-file-system-adapter#d43ad841581c2cc3ce47bbd1e8f11950ebdff027"
},
"devDependencies": { "devDependencies": {
"@types/emscripten": "^1.39.6", "@types/emscripten": "^1.39.6",
"@types/wicg-file-system-access": "^2020.9.5",
"@typescript-eslint/eslint-plugin": "^5.38.1", "@typescript-eslint/eslint-plugin": "^5.38.1",
"esbuild": "^0.15.7", "esbuild": "^0.15.7",
"eslint": "^8.24.0", "eslint": "^8.24.0",
@ -170,12 +172,6 @@
"integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==",
"dev": true "dev": true
}, },
"node_modules/@types/wicg-file-system-access": {
"version": "2020.9.5",
"resolved": "https://registry.npmjs.org/@types/wicg-file-system-access/-/wicg-file-system-access-2020.9.5.tgz",
"integrity": "sha512-UYK244awtmcUYQfs7FR8710MJcefL2WvkyHMjA8yJzxd1mo0Gfn88sRZ1Bls7hiUhA2w7ne1gpJ9T5g3G0wOyA==",
"dev": true
},
"node_modules/@typescript-eslint/eslint-plugin": { "node_modules/@typescript-eslint/eslint-plugin": {
"version": "5.38.1", "version": "5.38.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.38.1.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.38.1.tgz",
@ -1573,6 +1569,29 @@
"reusify": "^1.0.4" "reusify": "^1.0.4"
} }
}, },
"node_modules/fetch-blob": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz",
"integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/jimmywarting"
},
{
"type": "paypal",
"url": "https://paypal.me/jimmywarting"
}
],
"optional": true,
"dependencies": {
"node-domexception": "^1.0.0",
"web-streams-polyfill": "^3.0.3"
},
"engines": {
"node": "^12.20 || >= 14.13"
}
},
"node_modules/file-entry-cache": { "node_modules/file-entry-cache": {
"version": "6.0.1", "version": "6.0.1",
"resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz",
@ -2289,6 +2308,27 @@
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
"dev": true "dev": true
}, },
"node_modules/native-file-system-adapter": {
"version": "3.0.0",
"resolved": "git+ssh://git@github.com/jimmywarting/native-file-system-adapter.git#d43ad841581c2cc3ce47bbd1e8f11950ebdff027",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/jimmywarting"
},
{
"type": "paypal",
"url": "https://paypal.me/jimmywarting"
}
],
"license": "MIT",
"engines": {
"node": ">=14.8.0"
},
"optionalDependencies": {
"fetch-blob": "^3.2.0"
}
},
"node_modules/natural-compare": { "node_modules/natural-compare": {
"version": "1.4.0", "version": "1.4.0",
"resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz",
@ -2301,6 +2341,25 @@
"integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==", "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==",
"dev": true "dev": true
}, },
"node_modules/node-domexception": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz",
"integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/jimmywarting"
},
{
"type": "github",
"url": "https://paypal.me/jimmywarting"
}
],
"optional": true,
"engines": {
"node": ">=10.5.0"
}
},
"node_modules/normalize-package-data": { "node_modules/normalize-package-data": {
"version": "2.5.0", "version": "2.5.0",
"resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz",
@ -3196,6 +3255,15 @@
"spdx-expression-parse": "^3.0.0" "spdx-expression-parse": "^3.0.0"
} }
}, },
"node_modules/web-streams-polyfill": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.2.1.tgz",
"integrity": "sha512-e0MO3wdXWKrLbL0DgGnUV7WHVuw9OUvL4hjgnPkIeEvESk74gAITi5G606JtZPp39cd8HA9VQzCIvA49LpPN5Q==",
"optional": true,
"engines": {
"node": ">= 8"
}
},
"node_modules/which": { "node_modules/which": {
"version": "2.0.2", "version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
@ -3366,12 +3434,6 @@
"integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==",
"dev": true "dev": true
}, },
"@types/wicg-file-system-access": {
"version": "2020.9.5",
"resolved": "https://registry.npmjs.org/@types/wicg-file-system-access/-/wicg-file-system-access-2020.9.5.tgz",
"integrity": "sha512-UYK244awtmcUYQfs7FR8710MJcefL2WvkyHMjA8yJzxd1mo0Gfn88sRZ1Bls7hiUhA2w7ne1gpJ9T5g3G0wOyA==",
"dev": true
},
"@typescript-eslint/eslint-plugin": { "@typescript-eslint/eslint-plugin": {
"version": "5.38.1", "version": "5.38.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.38.1.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.38.1.tgz",
@ -4275,6 +4337,16 @@
"reusify": "^1.0.4" "reusify": "^1.0.4"
} }
}, },
"fetch-blob": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz",
"integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==",
"optional": true,
"requires": {
"node-domexception": "^1.0.0",
"web-streams-polyfill": "^3.0.3"
}
},
"file-entry-cache": { "file-entry-cache": {
"version": "6.0.1", "version": "6.0.1",
"resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz",
@ -4796,6 +4868,13 @@
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
"dev": true "dev": true
}, },
"native-file-system-adapter": {
"version": "git+ssh://git@github.com/jimmywarting/native-file-system-adapter.git#d43ad841581c2cc3ce47bbd1e8f11950ebdff027",
"from": "native-file-system-adapter@github:jimmywarting/native-file-system-adapter#d43ad841581c2cc3ce47bbd1e8f11950ebdff027",
"requires": {
"fetch-blob": "^3.2.0"
}
},
"natural-compare": { "natural-compare": {
"version": "1.4.0", "version": "1.4.0",
"resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz",
@ -4808,6 +4887,12 @@
"integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==", "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==",
"dev": true "dev": true
}, },
"node-domexception": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz",
"integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==",
"optional": true
},
"normalize-package-data": { "normalize-package-data": {
"version": "2.5.0", "version": "2.5.0",
"resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz",
@ -5446,6 +5531,12 @@
"spdx-expression-parse": "^3.0.0" "spdx-expression-parse": "^3.0.0"
} }
}, },
"web-streams-polyfill": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.2.1.tgz",
"integrity": "sha512-e0MO3wdXWKrLbL0DgGnUV7WHVuw9OUvL4hjgnPkIeEvESk74gAITi5G606JtZPp39cd8HA9VQzCIvA49LpPN5Q==",
"optional": true
},
"which": { "which": {
"version": "2.0.2", "version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",

4
src/Browser/Avalonia.Browser/webapp/package.json

@ -8,7 +8,6 @@
}, },
"devDependencies": { "devDependencies": {
"@types/emscripten": "^1.39.6", "@types/emscripten": "^1.39.6",
"@types/wicg-file-system-access": "^2020.9.5",
"@typescript-eslint/eslint-plugin": "^5.38.1", "@typescript-eslint/eslint-plugin": "^5.38.1",
"esbuild": "^0.15.7", "esbuild": "^0.15.7",
"eslint": "^8.24.0", "eslint": "^8.24.0",
@ -18,5 +17,8 @@
"eslint-plugin-promise": "^6.0.1", "eslint-plugin-promise": "^6.0.1",
"npm-run-all": "^4.1.5", "npm-run-all": "^4.1.5",
"typescript": "^4.8.3" "typescript": "^4.8.3"
},
"dependencies": {
"native-file-system-adapter": "github:jimmywarting/native-file-system-adapter#d43ad841581c2cc3ce47bbd1e8f11950ebdff027"
} }
} }

6
src/Browser/Avalonia.Browser/webapp/tsconfig.json

@ -1,14 +1,16 @@
{ {
"compilerOptions": { "compilerOptions": {
"target": "es2016", "target": "es2018",
"module": "es2020", "module": "es2020",
"strict": true, "strict": true,
"sourceMap": true, "sourceMap": true,
"noEmitOnError": true, "noEmitOnError": true,
"moduleResolution": "node",
"skipLibCheck": true,
"isolatedModules": true, // we need it for esbuild "isolatedModules": true, // we need it for esbuild
"lib": [ "lib": [
"dom", "dom",
"es2016", "es2018",
"esnext.asynciterable" "esnext.asynciterable"
] ]
}, },

4
src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindings/PropertyInfoAccessorFactory.cs

@ -121,7 +121,7 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions.CompiledBindings
{ {
if (_reference.TryGetTarget(out var o) && o is INotifyPropertyChanged inpc) if (_reference.TryGetTarget(out var o) && o is INotifyPropertyChanged inpc)
{ {
WeakEvents.PropertyChanged.Unsubscribe(inpc, this); WeakEvents.ThreadSafePropertyChanged.Unsubscribe(inpc, this);
} }
} }
@ -138,7 +138,7 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions.CompiledBindings
private void SubscribeToChanges() private void SubscribeToChanges()
{ {
if (_reference.TryGetTarget(out var o) && o is INotifyPropertyChanged inpc) if (_reference.TryGetTarget(out var o) && o is INotifyPropertyChanged inpc)
WeakEvents.PropertyChanged.Subscribe(inpc, this); WeakEvents.ThreadSafePropertyChanged.Subscribe(inpc, this);
} }
} }

75
src/Skia/Avalonia.Skia/DrawingContextImpl.cs

@ -34,9 +34,9 @@ namespace Avalonia.Skia
private GRContext _grContext; private GRContext _grContext;
public GRContext GrContext => _grContext; public GRContext GrContext => _grContext;
private ISkiaGpu _gpu; private ISkiaGpu _gpu;
private readonly SKPaint _strokePaint = SKPaintCache.Get(); private readonly SKPaint _strokePaint = SKPaintCache.Shared.Get();
private readonly SKPaint _fillPaint = SKPaintCache.Get(); private readonly SKPaint _fillPaint = SKPaintCache.Shared.Get();
private readonly SKPaint _boxShadowPaint = SKPaintCache.Get(); private readonly SKPaint _boxShadowPaint = SKPaintCache.Shared.Get();
private static SKShader s_acrylicNoiseShader; private static SKShader s_acrylicNoiseShader;
private readonly ISkiaGpuRenderSession _session; private readonly ISkiaGpuRenderSession _session;
private bool _leased = false; private bool _leased = false;
@ -186,13 +186,13 @@ namespace Avalonia.Skia
var s = sourceRect.ToSKRect(); var s = sourceRect.ToSKRect();
var d = destRect.ToSKRect(); var d = destRect.ToSKRect();
var paint = SKPaintCache.Get(); var paint = SKPaintCache.Shared.Get();
paint.Color = new SKColor(255, 255, 255, (byte)(255 * opacity * _currentOpacity)); paint.Color = new SKColor(255, 255, 255, (byte)(255 * opacity * _currentOpacity));
paint.FilterQuality = bitmapInterpolationMode.ToSKFilterQuality(); paint.FilterQuality = bitmapInterpolationMode.ToSKFilterQuality();
paint.BlendMode = _currentBlendingMode.ToSKBlendMode(); paint.BlendMode = _currentBlendingMode.ToSKBlendMode();
drawableImage.Draw(this, s, d, paint); drawableImage.Draw(this, s, d, paint);
SKPaintCache.ReturnReset(paint); SKPaintCache.Shared.ReturnReset(paint);
} }
/// <inheritdoc /> /// <inheritdoc />
@ -315,15 +315,20 @@ namespace Avalonia.Skia
var rc = rect.Rect.ToSKRect(); var rc = rect.Rect.ToSKRect();
var isRounded = rect.IsRounded; var isRounded = rect.IsRounded;
var needRoundRect = rect.IsRounded; var needRoundRect = rect.IsRounded;
using var skRoundRect = needRoundRect ? new SKRoundRect() : null; SKRoundRect skRoundRect = null;
if (needRoundRect) if (needRoundRect)
{
skRoundRect = SKRoundRectCache.Shared.Get();
skRoundRect.SetRectRadii(rc, skRoundRect.SetRectRadii(rc,
new[] new[]
{ {
rect.RadiiTopLeft.ToSKPoint(), rect.RadiiTopRight.ToSKPoint(), rect.RadiiTopLeft.ToSKPoint(),
rect.RadiiBottomRight.ToSKPoint(), rect.RadiiBottomLeft.ToSKPoint(), rect.RadiiTopRight.ToSKPoint(),
rect.RadiiBottomRight.ToSKPoint(),
rect.RadiiBottomLeft.ToSKPoint(),
}); });
}
if (material != null) if (material != null)
{ {
@ -332,6 +337,7 @@ namespace Avalonia.Skia
if (isRounded) if (isRounded)
{ {
Canvas.DrawRoundRect(skRoundRect, paint.Paint); Canvas.DrawRoundRect(skRoundRect, paint.Paint);
SKRoundRectCache.Shared.Return(skRoundRect);
} }
else else
{ {
@ -356,14 +362,11 @@ namespace Avalonia.Skia
var rc = rect.Rect.ToSKRect(); var rc = rect.Rect.ToSKRect();
var isRounded = rect.IsRounded; var isRounded = rect.IsRounded;
var needRoundRect = rect.IsRounded || (boxShadows.HasInsetShadows); var needRoundRect = rect.IsRounded || (boxShadows.HasInsetShadows);
using var skRoundRect = needRoundRect ? new SKRoundRect() : null; SKRoundRect skRoundRect = null;
if (needRoundRect) if (needRoundRect)
skRoundRect.SetRectRadii(rc, {
new[] skRoundRect = SKRoundRectCache.Shared.GetAndSetRadii(rc, rect);
{ }
rect.RadiiTopLeft.ToSKPoint(), rect.RadiiTopRight.ToSKPoint(),
rect.RadiiBottomRight.ToSKPoint(), rect.RadiiBottomLeft.ToSKPoint(),
});
foreach (var boxShadow in boxShadows) foreach (var boxShadow in boxShadows)
{ {
@ -378,7 +381,7 @@ namespace Avalonia.Skia
Canvas.Save(); Canvas.Save();
if (isRounded) if (isRounded)
{ {
using var shadowRect = new SKRoundRect(skRoundRect); var shadowRect = SKRoundRectCache.Shared.GetAndSetRadii(skRoundRect!.Rect, skRoundRect.Radii);
if (spread != 0) if (spread != 0)
shadowRect.Inflate(spread, spread); shadowRect.Inflate(spread, spread);
Canvas.ClipRoundRect(skRoundRect, Canvas.ClipRoundRect(skRoundRect,
@ -388,6 +391,7 @@ namespace Avalonia.Skia
Transform = oldTransform * Matrix.CreateTranslation(boxShadow.OffsetX, boxShadow.OffsetY); Transform = oldTransform * Matrix.CreateTranslation(boxShadow.OffsetX, boxShadow.OffsetY);
Canvas.DrawRoundRect(shadowRect, shadow.Paint); Canvas.DrawRoundRect(shadowRect, shadow.Paint);
Transform = oldTransform; Transform = oldTransform;
SKRoundRectCache.Shared.Return(shadowRect);
} }
else else
{ {
@ -433,7 +437,7 @@ namespace Avalonia.Skia
var outerRect = AreaCastingShadowInHole(rc, (float)boxShadow.Blur, spread, offsetX, offsetY); var outerRect = AreaCastingShadowInHole(rc, (float)boxShadow.Blur, spread, offsetX, offsetY);
Canvas.Save(); Canvas.Save();
using var shadowRect = new SKRoundRect(skRoundRect); var shadowRect = SKRoundRectCache.Shared.GetAndSetRadii(skRoundRect!.Rect, skRoundRect.Radii);
if (spread != 0) if (spread != 0)
shadowRect.Deflate(spread, spread); shadowRect.Deflate(spread, spread);
Canvas.ClipRoundRect(skRoundRect, Canvas.ClipRoundRect(skRoundRect,
@ -445,6 +449,7 @@ namespace Avalonia.Skia
Canvas.DrawRoundRectDifference(outerRRect, shadowRect, shadow.Paint); Canvas.DrawRoundRectDifference(outerRRect, shadowRect, shadow.Paint);
Transform = oldTransform; Transform = oldTransform;
Canvas.Restore(); Canvas.Restore();
SKRoundRectCache.Shared.Return(shadowRect);
} }
} }
} }
@ -466,6 +471,9 @@ namespace Avalonia.Skia
} }
} }
} }
if(isRounded)
SKRoundRectCache.Shared.Return(skRoundRect);
} }
/// <inheritdoc /> /// <inheritdoc />
@ -535,7 +543,24 @@ namespace Avalonia.Skia
{ {
CheckLease(); CheckLease();
Canvas.Save(); Canvas.Save();
Canvas.ClipRoundRect(clip.ToSKRoundRect(), antialias:true);
// Get the rounded rectangle
var rc = clip.Rect.ToSKRect();
// Get a round rect from the cache.
var roundRect = SKRoundRectCache.Shared.Get();
roundRect.SetRectRadii(rc,
new[]
{
clip.RadiiTopLeft.ToSKPoint(), clip.RadiiTopRight.ToSKPoint(),
clip.RadiiBottomRight.ToSKPoint(), clip.RadiiBottomLeft.ToSKPoint(),
});
Canvas.ClipRoundRect(roundRect, antialias:true);
// Should not need to reset as SetRectRadii overrides the values.
SKRoundRectCache.Shared.Return(roundRect);
} }
/// <inheritdoc /> /// <inheritdoc />
@ -569,9 +594,9 @@ namespace Avalonia.Skia
try try
{ {
// Return leased paints. // Return leased paints.
SKPaintCache.ReturnReset(_strokePaint); SKPaintCache.Shared.ReturnReset(_strokePaint);
SKPaintCache.ReturnReset(_fillPaint); SKPaintCache.Shared.ReturnReset(_fillPaint);
SKPaintCache.ReturnReset(_boxShadowPaint); SKPaintCache.Shared.ReturnReset(_boxShadowPaint);
if (_grContext != null) if (_grContext != null)
{ {
@ -633,7 +658,7 @@ namespace Avalonia.Skia
{ {
CheckLease(); CheckLease();
var paint = SKPaintCache.Get(); var paint = SKPaintCache.Shared.Get();
Canvas.SaveLayer(paint); Canvas.SaveLayer(paint);
_maskStack.Push(CreatePaint(paint, mask, bounds.Size)); _maskStack.Push(CreatePaint(paint, mask, bounds.Size));
@ -644,11 +669,11 @@ namespace Avalonia.Skia
{ {
CheckLease(); CheckLease();
var paint = SKPaintCache.Get(); var paint = SKPaintCache.Shared.Get();
paint.BlendMode = SKBlendMode.DstIn; paint.BlendMode = SKBlendMode.DstIn;
Canvas.SaveLayer(paint); Canvas.SaveLayer(paint);
SKPaintCache.ReturnReset(paint); SKPaintCache.Shared.ReturnReset(paint);
PaintWrapper paintWrapper; PaintWrapper paintWrapper;
using (paintWrapper = _maskStack.Pop()) using (paintWrapper = _maskStack.Pop())
@ -656,7 +681,7 @@ namespace Avalonia.Skia
Canvas.DrawPaint(paintWrapper.Paint); Canvas.DrawPaint(paintWrapper.Paint);
} }
// Return the paint wrapper's paint less the reset since the paint is already reset in the Dispose method above. // Return the paint wrapper's paint less the reset since the paint is already reset in the Dispose method above.
SKPaintCache.Return(paintWrapper.Paint); SKPaintCache.Shared.Return(paintWrapper.Paint);
Canvas.Restore(); Canvas.Restore();

4
src/Skia/Avalonia.Skia/GeometryImpl.cs

@ -81,12 +81,12 @@ namespace Avalonia.Skia
} }
else else
{ {
var paint = SKPaintCache.Get(); var paint = SKPaintCache.Shared.Get();
paint.IsStroke = true; paint.IsStroke = true;
paint.StrokeWidth = strokeWidth; paint.StrokeWidth = strokeWidth;
paint.GetFillPath(EffectivePath, strokePath); paint.GetFillPath(EffectivePath, strokePath);
SKPaintCache.ReturnReset(paint); SKPaintCache.Shared.ReturnReset(paint);
_pathCache.Cache(strokePath, strokeWidth, strokePath.TightBounds.ToAvaloniaRect()); _pathCache.Cache(strokePath, strokeWidth, strokePath.TightBounds.ToAvaloniaRect());
} }

4
src/Skia/Avalonia.Skia/Gpu/OpenGl/GlRenderTarget.cs

@ -13,7 +13,7 @@ namespace Avalonia.Skia
{ {
private readonly GRContext _grContext; private readonly GRContext _grContext;
private IGlPlatformSurfaceRenderTarget _surface; private IGlPlatformSurfaceRenderTarget _surface;
private static readonly SKSurfaceProperties _surfaceProperties = new SKSurfaceProperties(SKPixelGeometry.RgbHorizontal);
public GlRenderTarget(GRContext grContext, IGlContext glContext, IGlPlatformSurface glSurface) public GlRenderTarget(GRContext grContext, IGlContext glContext, IGlPlatformSurface glSurface)
{ {
_grContext = grContext; _grContext = grContext;
@ -92,7 +92,7 @@ namespace Avalonia.Skia
var renderTarget = new GRBackendRenderTarget(size.Width, size.Height, samples, disp.StencilSize, glInfo); var renderTarget = new GRBackendRenderTarget(size.Width, size.Height, samples, disp.StencilSize, glInfo);
var surface = SKSurface.Create(_grContext, renderTarget, var surface = SKSurface.Create(_grContext, renderTarget,
glSession.IsYFlipped ? GRSurfaceOrigin.TopLeft : GRSurfaceOrigin.BottomLeft, glSession.IsYFlipped ? GRSurfaceOrigin.TopLeft : GRSurfaceOrigin.BottomLeft,
colorType, new SKSurfaceProperties(SKPixelGeometry.RgbHorizontal)); colorType, _surfaceProperties);
success = true; success = true;

47
src/Skia/Avalonia.Skia/PlatformRenderInterface.cs

@ -76,13 +76,14 @@ namespace Avalonia.Skia
} }
var fontRenderingEmSize = (float)glyphRun.FontRenderingEmSize; var fontRenderingEmSize = (float)glyphRun.FontRenderingEmSize;
var skFont = new SKFont(glyphTypeface.Typeface, fontRenderingEmSize)
{ var skFont = SKFontCache.Shared.Get();
Size = fontRenderingEmSize,
Edging = SKFontEdging.Alias, skFont.Typeface = glyphTypeface.Typeface;
Hinting = SKFontHinting.None, skFont.Size = fontRenderingEmSize;
LinearMetrics = true skFont.Edging = SKFontEdging.Alias;
}; skFont.Hinting = SKFontHinting.None;
skFont.LinearMetrics = true;
SKPath path = new SKPath(); SKPath path = new SKPath();
@ -101,6 +102,8 @@ namespace Avalonia.Skia
currentX += glyphRun.GlyphInfos[i].GlyphAdvance; currentX += glyphRun.GlyphInfos[i].GlyphAdvance;
} }
SKFontCache.Shared.Return(skFont);
return new StreamGeometryImpl(path); return new StreamGeometryImpl(path);
} }
@ -224,20 +227,19 @@ namespace Avalonia.Skia
var glyphTypefaceImpl = glyphTypeface as GlyphTypefaceImpl; var glyphTypefaceImpl = glyphTypeface as GlyphTypefaceImpl;
var font = new SKFont var font = SKFontCache.Shared.Get();
{
LinearMetrics = true, font.LinearMetrics = true;
Subpixel = true, font.Subpixel = true;
Edging = SKFontEdging.SubpixelAntialias, font.Edging = SKFontEdging.SubpixelAntialias;
Hinting = SKFontHinting.Full, font.Hinting = SKFontHinting.Full;
Size = (float)fontRenderingEmSize, font.Size = (float)fontRenderingEmSize;
Typeface = glyphTypefaceImpl.Typeface, font.Typeface = glyphTypefaceImpl.Typeface;
Embolden = (glyphTypefaceImpl.FontSimulations & FontSimulations.Bold) != 0, font.Embolden = (glyphTypefaceImpl.FontSimulations & FontSimulations.Bold) != 0;
SkewX = (glyphTypefaceImpl.FontSimulations & FontSimulations.Oblique) != 0 ? -0.2f : 0 font.SkewX = (glyphTypefaceImpl.FontSimulations & FontSimulations.Oblique) != 0 ? -0.2f : 0;
};
var builder = new SKTextBlobBuilder();
var builder = SKTextBlobBuilderCache.Shared.Get();
var count = glyphInfos.Count; var count = glyphInfos.Count;
var runBuffer = builder.AllocatePositionedRun(font, count); var runBuffer = builder.AllocatePositionedRun(font, count);
@ -245,6 +247,8 @@ namespace Avalonia.Skia
var glyphSpan = runBuffer.GetGlyphSpan(); var glyphSpan = runBuffer.GetGlyphSpan();
var positionSpan = runBuffer.GetPositionSpan(); var positionSpan = runBuffer.GetPositionSpan();
SKFontCache.Shared.Return(font);
var width = 0.0; var width = 0.0;
for (int i = 0; i < count; i++) for (int i = 0; i < count; i++)
@ -261,8 +265,11 @@ namespace Avalonia.Skia
var scale = fontRenderingEmSize / glyphTypeface.Metrics.DesignEmHeight; var scale = fontRenderingEmSize / glyphTypeface.Metrics.DesignEmHeight;
var height = glyphTypeface.Metrics.LineSpacing * scale; var height = glyphTypeface.Metrics.LineSpacing * scale;
var skTextBlob = builder.Build();
SKTextBlobBuilderCache.Shared.Return(builder);
return new GlyphRunImpl(builder.Build(), new Size(width, height), baselineOrigin); return new GlyphRunImpl(skTextBlob, new Size(width, height), baselineOrigin);
} }
} }
} }

72
src/Skia/Avalonia.Skia/SKCacheBase.cs

@ -0,0 +1,72 @@
using System;
using System.Collections.Concurrent;
using SkiaSharp;
namespace Avalonia.Skia
{
/// <summary>
/// Cache base for Skia objects.
/// </summary>
internal abstract class SKCacheBase<TCachedItem, TCache>
where TCachedItem : IDisposable, new()
where TCache : new()
{
/// <summary>
/// Bag to hold the cached items.
/// </summary>
protected readonly ConcurrentBag<TCachedItem> Cache;
/// <summary>
/// Shared cache.
/// </summary>
public static readonly TCache Shared = new TCache();
protected SKCacheBase()
{
Cache = new ConcurrentBag<TCachedItem>();
}
/// <summary>
/// Gets a cached item for usage.
/// </summary>
/// <remarks>
/// If there is a available item in the cache, the cached item will be returned..
/// Otherwise a new cached item will be created.
/// </remarks>
/// <returns></returns>
public TCachedItem Get()
{
if (!Cache.TryTake(out var item))
{
item = new TCachedItem();
}
return item;
}
/// <summary>
/// Returns the item for reuse later.
/// </summary>
/// <remarks>
/// Do not use the item further.
/// Do not return the same item multiple times as that will break the cache.
/// </remarks>
/// <param name="item"></param>
public void Return(TCachedItem item)
{
Cache.Add(item);
}
/// <summary>
/// Clears and disposes all cached items.
/// </summary>
public void Clear()
{
while (Cache.TryTake(out var item))
{
item.Dispose();
}
}
}
}

13
src/Skia/Avalonia.Skia/SKFontCache.cs

@ -0,0 +1,13 @@
using System.Collections.Concurrent;
using SkiaSharp;
namespace Avalonia.Skia
{
/// <summary>
/// Cache for SKFonts.
/// </summary>
internal class SKFontCache : SKCacheBase<SKFont, SKFontCache>
{
}
}

58
src/Skia/Avalonia.Skia/SKPaintCache.cs

@ -6,46 +6,8 @@ namespace Avalonia.Skia
/// <summary> /// <summary>
/// Cache for SKPaints. /// Cache for SKPaints.
/// </summary> /// </summary>
internal static class SKPaintCache internal class SKPaintCache : SKCacheBase<SKPaint, SKPaintCache>
{ {
private static ConcurrentBag<SKPaint> s_cachedPaints;
static SKPaintCache()
{
s_cachedPaints = new ConcurrentBag<SKPaint>();
}
/// <summary>
/// Gets a SKPaint for usage.
/// </summary>
/// <remarks>
/// If a SKPaint is in the cache, that existing SKPaint will be returned.
/// Otherwise a new SKPaint will be created.
/// </remarks>
/// <returns></returns>
public static SKPaint Get()
{
if (!s_cachedPaints.TryTake(out var paint))
{
paint = new SKPaint();
}
return paint;
}
/// <summary>
/// Returns a SKPaint for reuse later.
/// </summary>
/// <remarks>
/// Do not use the paint further.
/// Do not return the same paint multiple times as that will break the cache.
/// </remarks>
/// <param name="paint"></param>
public static void Return(SKPaint paint)
{
s_cachedPaints.Add(paint);
}
/// <summary> /// <summary>
/// Returns a SKPaint and resets it for reuse later. /// Returns a SKPaint and resets it for reuse later.
/// </summary> /// </summary>
@ -54,23 +16,11 @@ namespace Avalonia.Skia
/// Do not return the same paint multiple times as that will break the cache. /// Do not return the same paint multiple times as that will break the cache.
/// Uses SKPaint.Reset() for reuse later. /// Uses SKPaint.Reset() for reuse later.
/// </remarks> /// </remarks>
/// <param name="paint"></param> /// <param name="paint">Paint to reset.</param>
public static void ReturnReset(SKPaint paint) public void ReturnReset(SKPaint paint)
{ {
paint.Reset(); paint.Reset();
s_cachedPaints.Add(paint); Cache.Add(paint);
} }
/// <summary>
/// Clears and disposes all cached paints.
/// </summary>
public static void Clear()
{
while (s_cachedPaints.TryTake(out var paint))
{
paint.Dispose();
}
}
} }
} }

99
src/Skia/Avalonia.Skia/SKRoundRectCache.cs

@ -0,0 +1,99 @@
using System.Buffers;
using System.Collections.Concurrent;
using System.Collections.Generic;
using SkiaSharp;
namespace Avalonia.Skia
{
/// <summary>
/// Cache for SKRoundRectCache.
/// </summary>
internal class SKRoundRectCache : SKCacheBase<SKRoundRect, SKRoundRectCache>
{
/// <summary>
/// Cache for points to use for setting the radii.
/// </summary>
private readonly ConcurrentBag<SKPoint[]> _radiiCache = new();
/// <summary>
/// Gets a cached SKRoundRect and sets it with the passed rectangle and Radii.
/// </summary>
/// <param name="rectangle">Rectangle size to set the cached rectangle to.</param>
/// <param name="roundedRect">Rounded rectangle to copy the radii from.</param>
/// <returns>Configured rounded rectangle</returns>
public SKRoundRect GetAndSetRadii(in SKRect rectangle, in RoundedRect roundedRect)
{
if (!Cache.TryTake(out var item))
{
item = new SKRoundRect();
}
// Try and acquire a cached point array.
if (!_radiiCache.TryTake(out var skArray))
{
skArray = new SKPoint[4];
}
skArray[0].X = (float)roundedRect.RadiiTopLeft.X;
skArray[0].Y = (float)roundedRect.RadiiTopLeft.Y;
skArray[1].X = (float)roundedRect.RadiiTopRight.X;
skArray[1].Y = (float)roundedRect.RadiiTopRight.Y;
skArray[2].X = (float)roundedRect.RadiiBottomRight.X;
skArray[2].Y = (float)roundedRect.RadiiBottomRight.Y;
skArray[3].X = (float)roundedRect.RadiiBottomLeft.X;
skArray[3].Y = (float)roundedRect.RadiiBottomLeft.Y;
item.SetRectRadii(rectangle, skArray);
// Add the array back to the cache.
_radiiCache.Add(skArray);
return item;
}
/// <summary>
/// Gets a cached SKRoundRect and sets it with the passed rectangle and Radii.
/// </summary>
/// <param name="rectangle">Rectangle size to set the cached rectangle to.</param>
/// <param name="radii">point array of radii.</param>
/// <returns>Configured rounded rectangle</returns>
public SKRoundRect GetAndSetRadii(in SKRect rectangle, in SKPoint[] radii)
{
if (!Cache.TryTake(out var item))
{
item = new SKRoundRect();
}
item.SetRectRadii(rectangle, radii);
return item;
}
/// <summary>
/// Returns a SKPaint and resets it for reuse later.
/// </summary>
/// <remarks>
/// Do not use the rect further.
/// Do not return the same rect multiple times as that will break the cache.
/// Uses SKRoundRect.SetEmpty(); for reuse later.
/// </remarks>
/// <param name="rect">Rectangle to reset</param>
public void ReturnReset(SKRoundRect rect)
{
rect.SetEmpty();
Cache.Add(rect);
}
/// <summary>
/// Clears and disposes all cached items.
/// </summary>
public new void Clear()
{
base.Clear();
// Clear out the cache of SKPoint arrays.
while (_radiiCache.TryTake(out var item))
{
}
}
}
}

13
src/Skia/Avalonia.Skia/SKTextBlobBuilderCache.cs

@ -0,0 +1,13 @@
using System.Collections.Concurrent;
using SkiaSharp;
namespace Avalonia.Skia
{
/// <summary>
/// Cache for SKTextBlobBuilder.
/// </summary>
internal class SKTextBlobBuilderCache : SKCacheBase<SKTextBlobBuilder, SKTextBlobBuilderCache>
{
}
}

6
src/Skia/Avalonia.Skia/TextShaperImpl.cs

@ -1,5 +1,6 @@
using System; using System;
using System.Buffers; using System.Buffers;
using System.Collections.Concurrent;
using System.Globalization; using System.Globalization;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using Avalonia.Media.TextFormatting; using Avalonia.Media.TextFormatting;
@ -13,6 +14,7 @@ namespace Avalonia.Skia
{ {
internal class TextShaperImpl : ITextShaperImpl internal class TextShaperImpl : ITextShaperImpl
{ {
private static readonly ConcurrentDictionary<int, Language> s_cachedLanguage = new();
public ShapedBuffer ShapeText(ReadOnlyMemory<char> text, TextShaperOptions options) public ShapedBuffer ShapeText(ReadOnlyMemory<char> text, TextShaperOptions options)
{ {
var textSpan = text.Span; var textSpan = text.Span;
@ -33,7 +35,9 @@ namespace Avalonia.Skia
buffer.Direction = (bidiLevel & 1) == 0 ? Direction.LeftToRight : Direction.RightToLeft; buffer.Direction = (bidiLevel & 1) == 0 ? Direction.LeftToRight : Direction.RightToLeft;
buffer.Language = new Language(culture ?? CultureInfo.CurrentCulture); var usedCulture = culture ?? CultureInfo.CurrentCulture;
buffer.Language = s_cachedLanguage.GetOrAdd(usedCulture.LCID, i => new Language(usedCulture));
var font = ((GlyphTypefaceImpl)typeface).Font; var font = ((GlyphTypefaceImpl)typeface).Font;

19
src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs

@ -1,7 +1,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics;
using System.IO; using System.IO;
using Avalonia.Controls;
using Avalonia.Controls.Platform.Surfaces; using Avalonia.Controls.Platform.Surfaces;
using Avalonia.Direct2D1.Media; using Avalonia.Direct2D1.Media;
using Avalonia.Direct2D1.Media.Imaging; using Avalonia.Direct2D1.Media.Imaging;
@ -55,15 +55,18 @@ namespace Avalonia.Direct2D1
return; return;
} }
#if DEBUG #if DEBUG
try if (Debugger.IsAttached)
{ {
Direct2D1Factory = new SharpDX.Direct2D1.Factory1( try
SharpDX.Direct2D1.FactoryType.MultiThreaded, {
Direct2D1Factory = new SharpDX.Direct2D1.Factory1(
SharpDX.Direct2D1.FactoryType.MultiThreaded,
SharpDX.Direct2D1.DebugLevel.Error); SharpDX.Direct2D1.DebugLevel.Error);
} }
catch catch
{ {
// // ignore, retry below without the debug layer
}
} }
#endif #endif
if (Direct2D1Factory == null) if (Direct2D1Factory == null)

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

Loading…
Cancel
Save