ソースを参照

Merge branch 'master' into feature/itemssource

Steven Kirk 2 年 前
コミット
a9fd9c7273
100 ファイル変更1894 行追加420 行削除
  1. 2 2
      .nuke/build.schema.json
  2. 3 2
      Avalonia.Desktop.slnf
  3. 21 0
      Avalonia.sln
  4. 17 2
      azure-pipelines-integrationtests.yml
  5. 16 1
      build/SourceGenerators.props
  6. 12 0
      native/Avalonia.Native/src/OSX/Avalonia.Native.OSX.xcodeproj/project.pbxproj
  7. 46 0
      native/Avalonia.Native/src/OSX/AvnTextInputMethod.h
  8. 41 0
      native/Avalonia.Native/src/OSX/AvnTextInputMethod.mm
  9. 20 0
      native/Avalonia.Native/src/OSX/AvnTextInputMethodDelegate.h
  10. 2 4
      native/Avalonia.Native/src/OSX/AvnView.h
  11. 74 18
      native/Avalonia.Native/src/OSX/AvnView.mm
  12. 1 1
      native/Avalonia.Native/src/OSX/AvnWindow.mm
  13. 1 1
      native/Avalonia.Native/src/OSX/Screens.mm
  14. 6 0
      native/Avalonia.Native/src/OSX/WindowBaseImpl.h
  15. 27 7
      native/Avalonia.Native/src/OSX/WindowBaseImpl.mm
  16. 2 0
      native/Avalonia.Native/src/OSX/WindowImpl.h
  17. 6 1
      native/Avalonia.Native/src/OSX/WindowImpl.mm
  18. 9 7
      nukebuild/Build.cs
  19. 5 0
      nukebuild/numerge.config
  20. 5 1
      packages/Avalonia/Avalonia.csproj
  21. 1 0
      samples/BindingDemo/BindingDemo.csproj
  22. 1 1
      samples/ControlCatalog.Android/Resources/values/styles.xml
  23. 8 1
      samples/ControlCatalog.Browser/app.css
  24. 2 0
      samples/ControlCatalog.NetCore/Program.cs
  25. 2 2
      samples/ControlCatalog.iOS/ControlCatalog.iOS.csproj
  26. 1 5
      samples/ControlCatalog.iOS/Info.plist
  27. 2 2
      samples/ControlCatalog/App.xaml.cs
  28. 3 11
      samples/ControlCatalog/ControlCatalog.csproj
  29. 37 1
      samples/ControlCatalog/MainView.xaml.cs
  30. 0 1
      samples/ControlCatalog/MainWindow.xaml.cs
  31. 1 1
      samples/ControlCatalog/Pages/ComboBoxPage.xaml.cs
  32. 3 2
      samples/ControlCatalog/Pages/ContextFlyoutPage.xaml.cs
  33. 16 2
      samples/ControlCatalog/Pages/FlyoutsPage.axaml
  34. 1 7
      samples/ControlCatalog/Pages/FlyoutsPage.axaml.cs
  35. 2 9
      samples/ControlCatalog/Pages/LabelsPage.axaml.cs
  36. 3 11
      samples/ControlCatalog/Pages/RefreshContainerPage.axaml.cs
  37. 3 10
      samples/ControlCatalog/Pages/RelativePanelPage.axaml.cs
  38. 8 12
      samples/ControlCatalog/Pages/ThemePage.axaml.cs
  39. 16 6
      samples/ControlCatalog/Pages/WindowCustomizationsPage.xaml
  40. 26 4
      samples/ControlCatalog/ViewModels/MainWindowViewModel.cs
  41. 7 0
      samples/Generators.Sandbox/App.xaml
  42. 20 0
      samples/Generators.Sandbox/App.xaml.cs
  43. 10 0
      samples/Generators.Sandbox/Controls/CustomTextBox.cs
  44. 45 0
      samples/Generators.Sandbox/Controls/SignUpView.xaml
  45. 54 0
      samples/Generators.Sandbox/Controls/SignUpView.xaml.cs
  46. 28 0
      samples/Generators.Sandbox/Generators.Sandbox.csproj
  47. 15 0
      samples/Generators.Sandbox/Program.cs
  48. 70 0
      samples/Generators.Sandbox/ViewModels/SignUpViewModel.cs
  49. 9 0
      samples/Generators.Sandbox/Views/SignUpView.xaml
  50. 28 0
      samples/Generators.Sandbox/Views/SignUpView.xaml.cs
  51. 1 0
      samples/GpuInterop/GpuInterop.csproj
  52. 41 34
      samples/IntegrationTestApp/ShowWindowTest.axaml
  53. 25 6
      samples/IntegrationTestApp/ShowWindowTest.axaml.cs
  54. 9 2
      samples/IntegrationTestApp/bundle.sh
  55. 1 0
      samples/MobileSandbox/MobileSandbox.csproj
  56. 1 0
      samples/PlatformSanityChecks/PlatformSanityChecks.csproj
  57. 1 0
      samples/Previewer/Previewer.csproj
  58. 1 0
      samples/ReactiveUIDemo/ReactiveUIDemo.csproj
  59. 1 0
      samples/RenderDemo/RenderDemo.csproj
  60. 1 0
      samples/Sandbox/Sandbox.csproj
  61. 1 0
      samples/VirtualizationDemo/VirtualizationDemo.csproj
  62. 15 0
      src/Android/Avalonia.Android/AvaloniaMainActivity.cs
  63. 6 0
      src/Android/Avalonia.Android/AvaloniaView.cs
  64. 235 0
      src/Android/Avalonia.Android/Platform/AndroidInsetsManager.cs
  65. 24 19
      src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs
  66. 33 17
      src/Android/Avalonia.Android/Platform/Storage/AndroidStorageItem.cs
  67. 6 1
      src/Avalonia.Base/AttachedProperty.cs
  68. 39 39
      src/Avalonia.Base/AvaloniaProperty.cs
  69. 9 1
      src/Avalonia.Base/AvaloniaPropertyMetadata.cs
  70. 1 1
      src/Avalonia.Base/AvaloniaPropertyRegistry.cs
  71. 2 0
      src/Avalonia.Base/DirectPropertyMetadata`1.cs
  72. 7 8
      src/Avalonia.Base/Input/GestureRecognizers/GestureRecognizerCollection.cs
  73. 4 8
      src/Avalonia.Base/Input/GestureRecognizers/PullGestureRecognizer.cs
  74. 18 34
      src/Avalonia.Base/Input/GestureRecognizers/ScrollGestureRecognizer.cs
  75. 1 0
      src/Avalonia.Base/Input/MouseDevice.cs
  76. 2 1
      src/Avalonia.Base/Input/PenDevice.cs
  77. 2 0
      src/Avalonia.Base/Input/PointerEventArgs.cs
  78. 20 13
      src/Avalonia.Base/Input/PointerOverPreProcessor.cs
  79. 1 0
      src/Avalonia.Base/Input/TouchDevice.cs
  80. 2 2
      src/Avalonia.Base/Media/DrawingGroup.cs
  81. 99 31
      src/Avalonia.Base/Media/FontManager.cs
  82. 290 0
      src/Avalonia.Base/Media/Fonts/EmbeddedFontCollection.cs
  83. 4 0
      src/Avalonia.Base/Media/Fonts/FontCollectionKey.cs
  84. 32 34
      src/Avalonia.Base/Media/Fonts/FontFamilyLoader.cs
  85. 33 0
      src/Avalonia.Base/Media/Fonts/IFontCollection.cs
  86. 107 0
      src/Avalonia.Base/Media/Fonts/SystemFontCollection.cs
  87. 7 7
      src/Avalonia.Base/Media/GlyphRun.cs
  88. 1 1
      src/Avalonia.Base/Media/GlyphRunDrawing.cs
  89. 20 0
      src/Avalonia.Base/Media/IGlyphTypeface.cs
  90. 0 2
      src/Avalonia.Base/Media/Imaging/CroppedBitmap.cs
  91. 1 1
      src/Avalonia.Base/Media/TextDecoration.cs
  92. 2 2
      src/Avalonia.Base/Media/TextFormatting/ShapedTextRun.cs
  93. 8 7
      src/Avalonia.Base/Media/TextFormatting/TextCharacters.cs
  94. 1 1
      src/Avalonia.Base/Media/TextFormatting/TextEllipsisHelper.cs
  95. 1 1
      src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs
  96. 1 1
      src/Avalonia.Base/Media/TextFormatting/TextLeadingPrefixCharacterEllipsis.cs
  97. 3 3
      src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs
  98. 12 1
      src/Avalonia.Base/Media/Typeface.cs
  99. 1 0
      src/Avalonia.Base/Platform/IDrawingContextImpl.cs
  100. 23 7
      src/Avalonia.Base/Platform/IFontManagerImpl.cs

+ 2 - 2
.nuke/build.schema.json

@@ -84,11 +84,11 @@
               "GenerateCppHeaders",
               "Package",
               "RunCoreLibsTests",
-              "RunDesignerTests",
               "RunHtmlPreviewerTests",
               "RunLeakTests",
               "RunRenderTests",
               "RunTests",
+              "RunToolsTests",
               "ZipFiles"
             ]
           }
@@ -123,11 +123,11 @@
               "GenerateCppHeaders",
               "Package",
               "RunCoreLibsTests",
-              "RunDesignerTests",
               "RunHtmlPreviewerTests",
               "RunLeakTests",
               "RunRenderTests",
               "RunTests",
+              "RunToolsTests",
               "ZipFiles"
             ]
           }

+ 3 - 2
Avalonia.Desktop.slnf

@@ -8,9 +8,9 @@
       "samples\\GpuInterop\\GpuInterop.csproj",
       "samples\\IntegrationTestApp\\IntegrationTestApp.csproj",
       "samples\\MiniMvvm\\MiniMvvm.csproj",
+      "samples\\ReactiveUIDemo\\ReactiveUIDemo.csproj",
       "samples\\SampleControls\\ControlSamples.csproj",
       "samples\\Sandbox\\Sandbox.csproj",
-      "samples\\ReactiveUIDemo\\ReactiveUIDemo.csproj",
       "src\\Avalonia.Base\\Avalonia.Base.csproj",
       "src\\Avalonia.Build.Tasks\\Avalonia.Build.Tasks.csproj",
       "src\\Avalonia.Controls.ColorPicker\\Avalonia.Controls.ColorPicker.csproj",
@@ -41,6 +41,7 @@
       "src\\Windows\\Avalonia.Direct2D1\\Avalonia.Direct2D1.csproj",
       "src\\Windows\\Avalonia.Win32.Interop\\Avalonia.Win32.Interop.csproj",
       "src\\Windows\\Avalonia.Win32\\Avalonia.Win32.csproj",
+      "src\\tools\\Avalonia.Generators\\Avalonia.Generators.csproj",
       "src\\tools\\DevAnalyzers\\DevAnalyzers.csproj",
       "src\\tools\\DevGenerators\\DevGenerators.csproj",
       "src\\tools\\PublicAnalyzers\\Avalonia.Analyzers.csproj",
@@ -63,4 +64,4 @@
       "tests\\Avalonia.UnitTests\\Avalonia.UnitTests.csproj"
     ]
   }
-}
+}

+ 21 - 0
Avalonia.sln

@@ -244,8 +244,14 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Controls.ItemsRepe
 EndProject
 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Controls.ItemsRepeater.UnitTests", "tests\Avalonia.Controls.ItemsRepeater.UnitTests\Avalonia.Controls.ItemsRepeater.UnitTests.csproj", "{F4E36AA8-814E-4704-BC07-291F70F45193}"
 EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.Generators", "src\tools\Avalonia.Generators\Avalonia.Generators.csproj", "{DDA28789-C21A-4654-86CE-D01E81F095C5}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.Generators.Tests", "tests\Avalonia.Generators.Tests\Avalonia.Generators.Tests.csproj", "{2D7C812B-7E73-4252-8EFD-BC8A4D5CCB9F}"
+EndProject
 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.Fonts.Inter", "src\Avalonia.Fonts.Inter\Avalonia.Fonts.Inter.csproj", "{13F1135D-BA1A-435C-9C5B-A368D1D63DE4}"
 EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Generators.Sandbox", "samples\Generators.Sandbox\Generators.Sandbox.csproj", "{A82AD1BC-EBE6-4FC3-A13B-D52A50297533}"
+EndProject
 Global
 	GlobalSection(SolutionConfigurationPlatforms) = preSolution
 		Debug|Any CPU = Debug|Any CPU
@@ -573,10 +579,22 @@ Global
 		{F4E36AA8-814E-4704-BC07-291F70F45193}.Debug|Any CPU.Build.0 = Debug|Any CPU
 		{F4E36AA8-814E-4704-BC07-291F70F45193}.Release|Any CPU.ActiveCfg = Release|Any CPU
 		{F4E36AA8-814E-4704-BC07-291F70F45193}.Release|Any CPU.Build.0 = Release|Any CPU
+		{DDA28789-C21A-4654-86CE-D01E81F095C5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{DDA28789-C21A-4654-86CE-D01E81F095C5}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{DDA28789-C21A-4654-86CE-D01E81F095C5}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{DDA28789-C21A-4654-86CE-D01E81F095C5}.Release|Any CPU.Build.0 = Release|Any CPU
+		{2D7C812B-7E73-4252-8EFD-BC8A4D5CCB9F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{2D7C812B-7E73-4252-8EFD-BC8A4D5CCB9F}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{2D7C812B-7E73-4252-8EFD-BC8A4D5CCB9F}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{2D7C812B-7E73-4252-8EFD-BC8A4D5CCB9F}.Release|Any CPU.Build.0 = Release|Any CPU
 		{13F1135D-BA1A-435C-9C5B-A368D1D63DE4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
 		{13F1135D-BA1A-435C-9C5B-A368D1D63DE4}.Debug|Any CPU.Build.0 = Debug|Any CPU
 		{13F1135D-BA1A-435C-9C5B-A368D1D63DE4}.Release|Any CPU.ActiveCfg = Release|Any CPU
 		{13F1135D-BA1A-435C-9C5B-A368D1D63DE4}.Release|Any CPU.Build.0 = Release|Any CPU
+		{A82AD1BC-EBE6-4FC3-A13B-D52A50297533}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{A82AD1BC-EBE6-4FC3-A13B-D52A50297533}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{A82AD1BC-EBE6-4FC3-A13B-D52A50297533}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{A82AD1BC-EBE6-4FC3-A13B-D52A50297533}.Release|Any CPU.Build.0 = Release|Any CPU
 	EndGlobalSection
 	GlobalSection(SolutionProperties) = preSolution
 		HideSolutionNode = FALSE
@@ -643,7 +661,10 @@ Global
 		{75C47156-C5D8-44BC-A5A7-E8657C2248D6} = {9B9E3891-2366-4253-A952-D08BCEB71098}
 		{C810060E-3809-4B74-A125-F11533AF9C1B} = {9B9E3891-2366-4253-A952-D08BCEB71098}
 		{C692FE73-43DB-49CE-87FC-F03ED61F25C9} = {4ED8B739-6F4E-4CD4-B993-545E6B5CE637}
+		{DDA28789-C21A-4654-86CE-D01E81F095C5} = {4ED8B739-6F4E-4CD4-B993-545E6B5CE637}
+		{2D7C812B-7E73-4252-8EFD-BC8A4D5CCB9F} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B}
 		{F4E36AA8-814E-4704-BC07-291F70F45193} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B}
+		{A82AD1BC-EBE6-4FC3-A13B-D52A50297533} = {9B9E3891-2366-4253-A952-D08BCEB71098}
 	EndGlobalSection
 	GlobalSection(ExtensibilityGlobals) = postSolution
 		SolutionGuid = {87366D66-1391-4D90-8999-95A620AD786A}

+ 17 - 2
azure-pipelines-integrationtests.yml

@@ -15,28 +15,41 @@ jobs:
       version: 7.0.101
       
   - script: system_profiler SPDisplaysDataType |grep Resolution
+    displayName: 'Get Resolution'
   
   - script: |
+      arch="x64"
+      if [[ $(uname -m) == 'arm64' ]]; then
+      arch="arm64"
+      fi
       sudo xcode-select -s /Applications/Xcode.app/Contents/Developer
       pkill node
-      appium &
+      pkill testmanagerd
+      appium > appium.out &
       pkill IntegrationTestApp
       ./build.sh CompileNative
       rm -rf $(osascript -e "POSIX path of (path to application id \"net.avaloniaui.avalonia.integrationtestapp\")")
       pkill IntegrationTestApp
       ./samples/IntegrationTestApp/bundle.sh
-      open -n ./samples/IntegrationTestApp/bin/Debug/net7.0/osx-arm64/publish/IntegrationTestApp.app
+      open -n ./samples/IntegrationTestApp/bin/Debug/net7.0/osx-$arch/publish/IntegrationTestApp.app
       pkill IntegrationTestApp
+    displayName: 'Build IntegrationTestApp'
 
   - task: DotNetCoreCLI@2
+    displayName: 'Run Integration Tests'
     inputs:
       command: 'test'
       projects: 'tests/Avalonia.IntegrationTests.Appium/Avalonia.IntegrationTests.Appium.csproj'
+      arguments: '-l "console;verbosity=detailed"'
 
   - script: |
       pkill IntegrationTestApp
       pkill node
+    displayName: 'Stop Appium'
 
+  - publish: appium.out
+    displayName: 'Publish appium logs on failure'
+    condition: failed()
 
 - job: Windows
   pool:
@@ -60,11 +73,13 @@ jobs:
     displayName: 'Start WinAppDriver'
   
   - task: DotNetCoreCLI@2
+    displayName: 'Build IntegrationTestApp'
     inputs:
       command: 'build'
       projects: 'samples/IntegrationTestApp/IntegrationTestApp.csproj'
 
   - task: DotNetCoreCLI@2
+    displayName: 'Run Integration Tests'
     retryCountOnTaskFailure: 3
     inputs:
       command: 'test'

+ 16 - 1
build/SourceGenerators.props

@@ -1,5 +1,10 @@
 <Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
-  <ItemGroup>
+  <PropertyGroup>
+    <IncludeDevGenerators Condition="'$(IncludeDevGenerators)' == ''">true</IncludeDevGenerators>
+    <IncludeAvaloniaGenerators Condition="'$(IncludeAvaloniaGenerators)' == ''">false</IncludeAvaloniaGenerators>
+  </PropertyGroup>
+
+  <ItemGroup Condition="'$(IncludeDevGenerators)' == 'true'">
     <ProjectReference 
       Include="$(MSBuildThisFileDirectory)/../src/tools/DevGenerators/DevGenerators.csproj"
       OutputItemType="Analyzer" 
@@ -7,4 +12,14 @@
       PrivateAssets="all" />
     <Compile Include="$(MSBuildThisFileDirectory)/../src/Shared/SourceGeneratorAttributes.cs" />
   </ItemGroup>
+
+  <ItemGroup Condition="'$(IncludeAvaloniaGenerators)' == 'true'">
+    <ProjectReference
+      Include="../../src/tools/Avalonia.Generators/Avalonia.Generators.csproj"
+      OutputItemType="Analyzer"
+      ReferenceOutputAssembly="false"
+      PrivateAssets="all" />
+  </ItemGroup>
+  <Import Project="$(MSBuildThisFileDirectory)/../src/tools/Avalonia.Generators/Avalonia.Generators.props"
+          Condition="'$(IncludeDevGenerators)' == 'true'" />
 </Project>

+ 12 - 0
native/Avalonia.Native/src/OSX/Avalonia.Native.OSX.xcodeproj/project.pbxproj

@@ -44,6 +44,9 @@
 		5B21A982216530F500CEE36E /* cursor.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5B21A981216530F500CEE36E /* cursor.mm */; };
 		5B8BD94F215BFEA6005ED2A7 /* clipboard.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5B8BD94E215BFEA6005ED2A7 /* clipboard.mm */; };
 		855EDC9F28C6546F00807998 /* PlatformBehaviorInhibition.mm in Sources */ = {isa = PBXBuildFile; fileRef = 855EDC9E28C6546F00807998 /* PlatformBehaviorInhibition.mm */; };
+		8D2F3512292F6AAE007FCF54 /* AvnTextInputMethodDelegate.h in Headers */ = {isa = PBXBuildFile; fileRef = 8D2F3511292F6AAE007FCF54 /* AvnTextInputMethodDelegate.h */; };
+		8D300D65292D0A6800320C49 /* AvnTextInputMethod.h in Headers */ = {isa = PBXBuildFile; fileRef = 8D300D64292D0A6800320C49 /* AvnTextInputMethod.h */; };
+		8D300D69292E1E5D00320C49 /* AvnTextInputMethod.mm in Sources */ = {isa = PBXBuildFile; fileRef = 8D300D68292E1E5D00320C49 /* AvnTextInputMethod.mm */; };
 		AB00E4F72147CA920032A60A /* main.mm in Sources */ = {isa = PBXBuildFile; fileRef = AB00E4F62147CA920032A60A /* main.mm */; };
 		AB1E522C217613570091CD71 /* OpenGL.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = AB1E522B217613570091CD71 /* OpenGL.framework */; };
 		AB661C1E2148230F00291242 /* AppKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = AB661C1D2148230F00291242 /* AppKit.framework */; };
@@ -97,6 +100,9 @@
 		5B8BD94E215BFEA6005ED2A7 /* clipboard.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = clipboard.mm; sourceTree = "<group>"; };
 		5BF943652167AD1D009CAE35 /* cursor.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = cursor.h; sourceTree = "<group>"; };
 		855EDC9E28C6546F00807998 /* PlatformBehaviorInhibition.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = PlatformBehaviorInhibition.mm; sourceTree = "<group>"; };
+		8D2F3511292F6AAE007FCF54 /* AvnTextInputMethodDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AvnTextInputMethodDelegate.h; sourceTree = "<group>"; };
+		8D300D64292D0A6800320C49 /* AvnTextInputMethod.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AvnTextInputMethod.h; sourceTree = "<group>"; };
+		8D300D68292E1E5D00320C49 /* AvnTextInputMethod.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = AvnTextInputMethod.mm; sourceTree = "<group>"; };
 		AB00E4F62147CA920032A60A /* main.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = main.mm; sourceTree = "<group>"; };
 		AB1E522B217613570091CD71 /* OpenGL.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = OpenGL.framework; path = System/Library/Frameworks/OpenGL.framework; sourceTree = SDKROOT; };
 		AB661C1D2148230F00291242 /* AppKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AppKit.framework; path = System/Library/Frameworks/AppKit.framework; sourceTree = SDKROOT; };
@@ -143,6 +149,9 @@
 			isa = PBXGroup;
 			children = (
 				855EDC9E28C6546F00807998 /* PlatformBehaviorInhibition.mm */,
+				8D2F3511292F6AAE007FCF54 /* AvnTextInputMethodDelegate.h */,
+				8D300D68292E1E5D00320C49 /* AvnTextInputMethod.mm */,
+				8D300D64292D0A6800320C49 /* AvnTextInputMethod.h */,
 				BC11A5BC2608D58F0017BAD0 /* automation.h */,
 				BC11A5BD2608D58F0017BAD0 /* automation.mm */,
 				1A1852DB23E05814008F0DED /* deadlock.mm */,
@@ -213,6 +222,8 @@
 				1839171DCC651B0638603AC4 /* INSWindowHolder.h in Headers */,
 				183919D91DB9AAB5D700C2EA /* WindowImpl.h in Headers */,
 				18391CF07316F819E76B617C /* IWindowStateChanged.h in Headers */,
+				8D300D65292D0A6800320C49 /* AvnTextInputMethod.h in Headers */,
+				8D2F3512292F6AAE007FCF54 /* AvnTextInputMethodDelegate.h in Headers */,
 				18391C28BF1823B5464FDD36 /* ResizeScope.h in Headers */,
 				18391ED5F611FF62C45F196D /* AvnView.h in Headers */,
 				18391E1381E2D5BFD60265A9 /* AutoFitContentView.h in Headers */,
@@ -293,6 +304,7 @@
 				37E2330F21583241000CB7E2 /* KeyTransform.mm in Sources */,
 				855EDC9F28C6546F00807998 /* PlatformBehaviorInhibition.mm in Sources */,
 				520624B322973F4100C4DCEF /* menu.mm in Sources */,
+				8D300D69292E1E5D00320C49 /* AvnTextInputMethod.mm in Sources */,
 				37A517B32159597E00FBA241 /* Screens.mm in Sources */,
 				1AFD334123E03C4F0042899B /* controlhost.mm in Sources */,
 				1A465D10246AB61600C5858B /* dnd.mm in Sources */,

+ 46 - 0
native/Avalonia.Native/src/OSX/AvnTextInputMethod.h

@@ -0,0 +1,46 @@
+//
+//  AvnTextInputMethod.h
+//  Avalonia.Native.OSX
+//
+//  Created by Benedikt Stebner on 22.11.22.
+//  Copyright © 2022 Avalonia. All rights reserved.
+//
+
+#ifndef AvnTextInputMethod_h
+#define AvnTextInputMethod_h
+
+#import <Foundation/Foundation.h>
+
+#include "com.h"
+#include "comimpl.h"
+#include "avalonia-native.h"
+#import "AvnTextInputMethodDelegate.h"
+
+class AvnTextInputMethod: public virtual ComObject, public virtual IAvnTextInputMethod{
+private:
+    id<AvnTextInputMethodDelegate> _inputMethodDelegate;
+public:
+    FORWARD_IUNKNOWN()
+    
+    BEGIN_INTERFACE_MAP()
+    INTERFACE_MAP_ENTRY(IAvnTextInputMethod, IID_IAvnTextInputMethod)
+    END_INTERFACE_MAP()
+    
+    virtual ~AvnTextInputMethod();
+    
+    AvnTextInputMethod(id<AvnTextInputMethodDelegate> inputMethodDelegate);
+    
+    bool IsActive ();
+    
+    HRESULT SetClient (IAvnTextInputMethodClient* client) override;
+    
+    virtual void Reset () override;
+    
+    virtual void SetCursorRect (AvnRect rect) override;
+    
+    virtual void SetSurroundingText (char* text, int anchorOffset, int cursorOffset) override;
+    
+public:
+    ComPtr<IAvnTextInputMethodClient> Client;
+};
+#endif /* AvnTextInputMethod_h */

+ 41 - 0
native/Avalonia.Native/src/OSX/AvnTextInputMethod.mm

@@ -0,0 +1,41 @@
+//
+//  AvnTextInputMethod.mm
+//  Avalonia.Native.OSX
+//
+//  Created by Benedikt Stebner on 23.11.22.
+//  Copyright © 2022 Avalonia. All rights reserved.
+//
+
+#include "AvnTextInputMethod.h"
+
+AvnTextInputMethod::~AvnTextInputMethod() {
+    Client = nullptr;
+}
+
+AvnTextInputMethod::AvnTextInputMethod(id<AvnTextInputMethodDelegate> inputMethodDelegate) {
+    _inputMethodDelegate = inputMethodDelegate;
+}
+
+bool AvnTextInputMethod::IsActive() {
+    return Client != nullptr;
+}
+
+HRESULT AvnTextInputMethod::SetClient(IAvnTextInputMethodClient *client) {
+    START_COM_CALL;
+    
+    Client = client;
+    
+    return S_OK;
+}
+
+void AvnTextInputMethod::Reset() {
+}
+
+void AvnTextInputMethod::SetSurroundingText(char* text, int anchorOffset, int cursorOffset) {
+    [_inputMethodDelegate setText:[NSString stringWithUTF8String:text]];
+    [_inputMethodDelegate setSelection: anchorOffset : cursorOffset];
+}
+
+void AvnTextInputMethod::SetCursorRect(AvnRect rect) {
+    [_inputMethodDelegate setCursorRect: rect];
+}

+ 20 - 0
native/Avalonia.Native/src/OSX/AvnTextInputMethodDelegate.h

@@ -0,0 +1,20 @@
+//
+//  AvnTextInputMethodHost.h
+//  Avalonia.Native.OSX
+//
+//  Created by Benedikt Stebner on 24.11.22.
+//  Copyright © 2022 Avalonia. All rights reserved.
+//
+
+#ifndef AvnTextInputMethodHost_h
+#define AvnTextInputMethodHost_h
+
+@protocol AvnTextInputMethodDelegate
+@required
+-(void) setText:(NSString* _Nonnull) text;
+-(void) setCursorRect:(AvnRect) cursorRect;
+-(void) setSelection: (int) start : (int) end;
+
+@end
+
+#endif /* AvnTextInputMethodHost_h */

+ 2 - 4
native/Avalonia.Native/src/OSX/AvnView.h

@@ -5,8 +5,6 @@
 #pragma once
 #import <Foundation/Foundation.h>
 
-
-#import <Foundation/Foundation.h>
 #import <AppKit/AppKit.h>
 #include "common.h"
 #include "WindowImpl.h"
@@ -14,7 +12,7 @@
 
 @class AvnAccessibilityElement;
 
-@interface AvnView : NSView<NSTextInputClient, NSDraggingDestination>
+@interface AvnView : NSView<NSTextInputClient, NSDraggingDestination, AvnTextInputMethodDelegate>
 -(AvnView* _Nonnull) initWithParent: (WindowBaseImpl* _Nonnull) parent;
 -(NSEvent* _Nonnull) lastMouseDownEvent;
 -(AvnPoint) translateLocalPoint:(AvnPoint)pt;
@@ -24,4 +22,4 @@
 -(AvnPlatformResizeReason) getResizeReason;
 -(void) setResizeReason:(AvnPlatformResizeReason)reason;
 + (AvnPoint)toAvnPoint:(CGPoint)p;
-@end
+@end

+ 74 - 18
native/Avalonia.Native/src/OSX/AvnView.mm

@@ -12,6 +12,7 @@
 {
     ComPtr<WindowBaseImpl> _parent;
     NSTrackingArea* _area;
+    NSMutableAttributedString* _markedText;
     bool _isLeftPressed, _isMiddlePressed, _isRightPressed, _isXButton1Pressed, _isXButton2Pressed;
     AvnInputModifiers _modifierState;
     NSEvent* _lastMouseDownEvent;
@@ -20,6 +21,9 @@
     NSObject<IRenderTarget>* _renderTarget;
     AvnPlatformResizeReason _resizeReason;
     AvnAccessibilityElement* _accessibilityChild;
+    NSRect _cursorRect;
+    NSMutableString* _text;
+    NSRange _selection;
 }
 
 - (void)onClosed
@@ -127,11 +131,8 @@
         [self updateRenderTarget];
 
         auto reason = [self inLiveResize] ? ResizeUser : _resizeReason;
-        
-        if(_parent->IsShown())
-        {
-            _parent->BaseEvents->Resized(AvnSize{newSize.width, newSize.height}, reason);
-        }
+
+        _parent->BaseEvents->Resized(AvnSize{newSize.width, newSize.height}, reason);
     }
 }
 
@@ -521,7 +522,7 @@
 - (void)keyDown:(NSEvent *)event
 {
     [self keyboardEvent:event withType:KeyDown];
-    [[self inputContext] handleEvent:event];
+    _lastKeyHandled = [[self inputContext] handleEvent:event];
     [super keyDown:event];
 }
 
@@ -560,27 +561,50 @@
 
 - (BOOL)hasMarkedText
 {
-    return _lastKeyHandled;
+    return [_markedText length] > 0;
 }
 
 - (NSRange)markedRange
 {
+    if([_markedText length] > 0)
+        return NSMakeRange(0, [_markedText length] - 1);
     return NSMakeRange(NSNotFound, 0);
 }
 
 - (NSRange)selectedRange
 {
-    return NSMakeRange(NSNotFound, 0);
+    return _selection;
 }
 
 - (void)setMarkedText:(id)string selectedRange:(NSRange)selectedRange replacementRange:(NSRange)replacementRange
 {
-
+    if([string isKindOfClass:[NSAttributedString class]])
+    {
+        _markedText = [[NSMutableAttributedString alloc] initWithAttributedString:string];
+    }
+    else
+    {
+        _markedText = [[NSMutableAttributedString alloc] initWithString:string];
+    }
+    
+    if(!_parent->InputMethod->IsActive()){
+        return;
+    }
+    
+    _parent->InputMethod->Client->SetPreeditText((char*)[_markedText.string UTF8String]);
 }
 
 - (void)unmarkText
 {
-
+    [[_markedText mutableString] setString:@""];
+    
+    if(!_parent->InputMethod->IsActive()){
+        return;
+    }
+    
+    _parent->InputMethod->Client->SetPreeditText(nullptr);
+    
+    [[self inputContext] discardMarkedText];
 }
 
 - (NSArray<NSString *> *)validAttributesForMarkedText
@@ -590,30 +614,38 @@
 
 - (NSAttributedString *)attributedSubstringForProposedRange:(NSRange)range actualRange:(NSRangePointer)actualRange
 {
-    return [NSAttributedString new];
+    return nullptr;
 }
 
 - (void)insertText:(id)string replacementRange:(NSRange)replacementRange
 {
-    if(!_lastKeyHandled)
-    {
+    //[_text replaceCharactersInRange:replacementRange withString:string];
+    
+    [self unmarkText];
+    
+    //if(!_lastKeyHandled)
+    //{
         if(_parent != nullptr)
         {
             _lastKeyHandled = _parent->BaseEvents->RawTextInputEvent(0, [string UTF8String]);
         }
-    }
+    //}
+    
+    [[self inputContext] invalidateCharacterCoordinates];
 }
 
 - (NSUInteger)characterIndexForPoint:(NSPoint)point
 {
-    return 0;
+    return NSNotFound;
 }
 
 - (NSRect)firstRectForCharacterRange:(NSRange)range actualRange:(NSRangePointer)actualRange
 {
-    CGRect result = { 0 };
-
-    return result;
+    if(!_parent->InputMethod->IsActive()){
+        return NSZeroRect;
+    }
+    
+    return _cursorRect;
 }
 
 - (NSDragOperation)triggerAvnDragEvent: (AvnDragEventType) type info: (id <NSDraggingInfo>)info
@@ -718,4 +750,28 @@
     return [[self accessibilityChild] accessibilityFocusedUIElement];
 }
 
+- (void) setText:(NSString *)text{
+    [_text setString:text];
+    
+    [[self inputContext] discardMarkedText];
+}
+
+- (void) setSelection:(int)start :(int)end{
+    _selection = NSMakeRange(start, end - start);
+    
+    [[self inputContext] invalidateCharacterCoordinates];
+}
+
+- (void) setCursorRect:(AvnRect)rect{
+    NSRect cursorRect = ToNSRect(rect);
+    NSRect windowRectOnScreen = [[self window] convertRectToScreen:self.frame];
+    
+    windowRectOnScreen.size = cursorRect.size;
+    windowRectOnScreen.origin = NSMakePoint(windowRectOnScreen.origin.x + cursorRect.origin.x, windowRectOnScreen.origin.y + self.frame.size.height - cursorRect.origin.y - cursorRect.size.height);
+    
+    _cursorRect = windowRectOnScreen;
+    
+    [[self inputContext] invalidateCharacterCoordinates];
+}
+
 @end

+ 1 - 1
native/Avalonia.Native/src/OSX/AvnWindow.mm

@@ -394,7 +394,7 @@
 
 - (BOOL)windowShouldZoom:(NSWindow *_Nonnull)window toFrame:(NSRect)newFrame
 {
-    return true;
+    return _parent->CanZoom();
 }
 
 -(void)windowDidResignKey:(NSNotification *)notification

+ 1 - 1
native/Avalonia.Native/src/OSX/Screens.mm

@@ -41,7 +41,7 @@ public:
             ret->WorkingArea.X = [screen visibleFrame].origin.x;
             ret->WorkingArea.Y = ConvertPointY(ToAvnPoint([screen visibleFrame].origin)).Y - ret->WorkingArea.Height;
             
-            ret->Scaling = [screen backingScaleFactor];
+            ret->Scaling = 1;
             
             ret->IsPrimary = index == 0;
             

+ 6 - 0
native/Avalonia.Native/src/OSX/WindowBaseImpl.h

@@ -8,6 +8,7 @@
 
 #include "rendertarget.h"
 #include "INSWindowHolder.h"
+#include "AvnTextInputMethod.h"
 
 @class AutoFitContentView;
 @class AvnMenu;
@@ -103,7 +104,11 @@ BEGIN_INTERFACE_MAP()
     id<AvnWindowProtocol> GetWindowProtocol ();
                            
     virtual void BringToFront ();
+                           
+    virtual HRESULT GetInputMethod(IAvnTextInputMethod **retOut) override;
 
+    virtual bool CanZoom() { return false; }
+                           
 protected:
     virtual NSWindowStyleMask CalculateStyleMask() = 0;
     virtual void UpdateStyle();
@@ -130,6 +135,7 @@ public:
     NSObject <IRenderTarget> *renderTarget;
     NSWindow * Window;
     ComPtr<IAvnWindowBaseEvents> BaseEvents;
+    ComPtr<AvnTextInputMethod> InputMethod;
     AvnView *View;
 };
 

+ 27 - 7
native/Avalonia.Native/src/OSX/WindowBaseImpl.mm

@@ -4,6 +4,7 @@
 //
 
 #import <AppKit/AppKit.h>
+#import <Cocoa/Cocoa.h>
 #include "common.h"
 #include "AvnView.h"
 #include "menu.h"
@@ -14,6 +15,7 @@
 #import "WindowProtocol.h"
 #import "WindowInterfaces.h"
 #include "WindowBaseImpl.h"
+#include "AvnTextInputMethod.h"
 
 
 WindowBaseImpl::~WindowBaseImpl() {
@@ -28,6 +30,7 @@ WindowBaseImpl::WindowBaseImpl(IAvnWindowBaseEvents *events, IAvnGlContext *gl,
     _glContext = gl;
     renderTarget = [[IOSurfaceRenderTarget alloc] initWithOpenGlContext:gl];
     View = [[AvnView alloc] initWithParent:this];
+    InputMethod = new AvnTextInputMethod(View);
     StandardContainer = [[AutoFitContentView new] initWithContent:View];
 
     lastPositionSet = { 0, 0 };
@@ -293,15 +296,24 @@ HRESULT WindowBaseImpl::Resize(double x, double y, AvnPlatformResizeReason reaso
         }
 
         @try {
-            if(x != lastSize.width || y != lastSize.height) {
-                lastSize = NSSize{x, y};
-
+            if(x != lastSize.width || y != lastSize.height)
+            {
                 if (!_shown) {
-                    BaseEvents->Resized(AvnSize{x, y}, reason);
-                } else if (Window != nullptr) {
-                    [Window setContentSize:lastSize];
-                    [Window invalidateShadow];
+                    auto screenSize = [Window screen].visibleFrame.size;
+
+                    if (x > screenSize.width) {
+                        x = screenSize.width;
+                    }
+
+                    if (y > screenSize.height) {
+                        y = screenSize.height;
+                    }
                 }
+
+                lastSize = NSSize{x, y};
+
+                [Window setContentSize:lastSize];
+                [Window invalidateShadow];
             }
         }
         @finally {
@@ -595,6 +607,14 @@ void WindowBaseImpl::BringToFront()
     // do nothing.
 }
 
+HRESULT WindowBaseImpl::GetInputMethod(IAvnTextInputMethod **retOut) {
+    START_COM_CALL;
+
+    *retOut = InputMethod;
+
+    return S_OK;
+}
+
 extern IAvnWindow* CreateAvnWindow(IAvnWindowEvents*events, IAvnGlContext* gl)
 {
     @autoreleasepool

+ 2 - 0
native/Avalonia.Native/src/OSX/WindowImpl.h

@@ -97,6 +97,8 @@ BEGIN_INTERFACE_MAP()
     
     bool CanBecomeKeyWindow ();
 
+    bool CanZoom() override { return _isEnabled && _canResize; }
+    
 protected:
     virtual NSWindowStyleMask CalculateStyleMask() override;
     void UpdateStyle () override;

+ 6 - 1
native/Avalonia.Native/src/OSX/WindowImpl.mm

@@ -54,6 +54,11 @@ HRESULT WindowImpl::Show(bool activate, bool isDialog) {
 
         WindowBaseImpl::Show(activate, isDialog);
         GetWindowState(&_actualWindowState);
+
+        if(IsZoomed()) {
+            _lastWindowState = _actualWindowState;
+        }
+
         return SetWindowState(_lastWindowState);
     }
 }
@@ -617,5 +622,5 @@ void WindowImpl::UpdateStyle() {
     [miniaturizeButton setHidden:!hasTrafficLights];
     [miniaturizeButton setEnabled:_isEnabled];
     [zoomButton setHidden:!hasTrafficLights];
-    [zoomButton setEnabled:_isEnabled && _canResize];
+    [zoomButton setEnabled:CanZoom()];
 }

+ 9 - 7
nukebuild/Build.cs

@@ -165,10 +165,10 @@ partial class Build : NukeBuild
         foreach (var fw in targetFrameworks)
         {
             if (fw.StartsWith("net4")
-                && RuntimeInformation.IsOSPlatform(OSPlatform.Linux)
+                && (RuntimeInformation.IsOSPlatform(OSPlatform.Linux) || RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
                 && Environment.GetEnvironmentVariable("FORCE_LINUX_TESTS") != "1")
             {
-                Information($"Skipping {projectName} ({fw}) tests on Linux - https://github.com/mono/mono/issues/13969");
+                Information($"Skipping {projectName} ({fw}) tests on *nix - https://github.com/mono/mono/issues/13969");
                 continue;
             }
 
@@ -220,16 +220,18 @@ partial class Build : NukeBuild
         .Executes(() =>
         {
             RunCoreTest("Avalonia.Skia.RenderTests");
-            if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
+            if (Parameters.IsRunningOnWindows)
                 RunCoreTest("Avalonia.Direct2D1.RenderTests");
         });
 
-    Target RunDesignerTests => _ => _
-        .OnlyWhenStatic(() => !Parameters.SkipTests && Parameters.IsRunningOnWindows)
+    Target RunToolsTests => _ => _
+        .OnlyWhenStatic(() => !Parameters.SkipTests)
         .DependsOn(Compile)
         .Executes(() =>
         {
-            RunCoreTest("Avalonia.DesignerSupport.Tests");
+            RunCoreTest("Avalonia.Generators.Tests");
+            if (Parameters.IsRunningOnWindows)
+                RunCoreTest("Avalonia.DesignerSupport.Tests");
         });
 
     Target RunLeakTests => _ => _
@@ -276,7 +278,7 @@ partial class Build : NukeBuild
     Target RunTests => _ => _
         .DependsOn(RunCoreLibsTests)
         .DependsOn(RunRenderTests)
-        .DependsOn(RunDesignerTests)
+        .DependsOn(RunToolsTests)
         .DependsOn(RunHtmlPreviewerTests)
         .DependsOn(RunLeakTests);
 

+ 5 - 0
nukebuild/numerge.config

@@ -11,6 +11,11 @@
           "Id": "Avalonia.Build.Tasks",
           "IgnoreMissingFrameworkBinaries": true,
           "DoNotMergeDependencies": true
+        },
+        {
+          "Id": "Avalonia.Generators",
+          "IgnoreMissingFrameworkBinaries": true,
+          "DoNotMergeDependencies": true
         }
       ]
     }

+ 5 - 1
packages/Avalonia/Avalonia.csproj

@@ -6,11 +6,15 @@
 
   <ItemGroup>
       <ProjectReference Include="../../src/Avalonia.Remote.Protocol/Avalonia.Remote.Protocol.csproj" />
-      <ProjectReference Include="../../src/Avalonia.Build.Tasks/Avalonia.Build.Tasks.csproj" >
+      <ProjectReference Include="../../src/Avalonia.Build.Tasks/Avalonia.Build.Tasks.csproj">
         <PrivateAssets>all</PrivateAssets>
         <SkipGetTargetFrameworkProperties>true</SkipGetTargetFrameworkProperties>
         <SetTargetFramework>TargetFramework=netstandard2.0</SetTargetFramework>
       </ProjectReference>
+      <ProjectReference Include="..\..\src\tools\Avalonia.Generators\Avalonia.Generators.csproj"
+                        ReferenceOutputAssembly="false"
+                        PrivateAssets="all"
+                        OutputItemType="Analyzer" />
   </ItemGroup>
 
   <PropertyGroup>

+ 1 - 0
samples/BindingDemo/BindingDemo.csproj

@@ -5,6 +5,7 @@
   </PropertyGroup>
   <ItemGroup>
     <ProjectReference Include="..\..\src\Avalonia.Diagnostics\Avalonia.Diagnostics.csproj" />
+    <ProjectReference Include="..\..\src\Avalonia.Fonts.Inter\Avalonia.Fonts.Inter.csproj" />
     <ProjectReference Include="..\..\src\Avalonia.Themes.Fluent\Avalonia.Themes.Fluent.csproj" />
     <ProjectReference Include="..\..\src\Linux\Avalonia.LinuxFramebuffer\Avalonia.LinuxFramebuffer.csproj" />
     <ProjectReference Include="..\MiniMvvm\MiniMvvm.csproj" />

+ 1 - 1
samples/ControlCatalog.Android/Resources/values/styles.xml

@@ -4,7 +4,7 @@
   <style name="MyTheme">
   </style>
 
-  <style name="MyTheme.NoActionBar" parent="@style/Theme.AppCompat.NoActionBar">
+  <style name="MyTheme.NoActionBar" parent="@style/Theme.AppCompat.DayNight.NoActionBar">
     <item name="android:windowActionBar">false</item>
     <item name="android:windowNoTitle">true</item>
   </style>

+ 8 - 1
samples/ControlCatalog.Browser/app.css

@@ -1,4 +1,11 @@
-#out {
+:root {
+    --sat: env(safe-area-inset-top);
+    --sar: env(safe-area-inset-right);
+    --sab: env(safe-area-inset-bottom);
+    --sal: env(safe-area-inset-left);
+}
+
+#out {
     height: 100vh;
     width: 100vw
 }

+ 2 - 0
samples/ControlCatalog.NetCore/Program.cs

@@ -7,6 +7,7 @@ using System.Threading.Tasks;
 using Avalonia;
 using Avalonia.Controls;
 using Avalonia.Controls.ApplicationLifetimes;
+using Avalonia.Fonts.Inter;
 using Avalonia.Headless;
 using Avalonia.LogicalTree;
 using Avalonia.Threading;
@@ -124,6 +125,7 @@ namespace ControlCatalog.NetCore
                     EnableIme = true
                 })
                 .UseSkia()
+                .WithInterFont()
                 .AfterSetup(builder =>
                 {
                     builder.Instance!.AttachDevTools(new Avalonia.Diagnostics.DevToolsOptions()

+ 2 - 2
samples/ControlCatalog.iOS/ControlCatalog.iOS.csproj

@@ -3,7 +3,7 @@
     <OutputType>Exe</OutputType>
     <ProvisioningType>manual</ProvisioningType>
     <TargetFramework>net6.0-ios</TargetFramework>
-    <SupportedOSPlatformVersion>10.0</SupportedOSPlatformVersion>
+    <SupportedOSPlatformVersion>13.0</SupportedOSPlatformVersion>
     <!-- temporal workaround for our GL interface backend -->
     <UseInterpreter>True</UseInterpreter>
     <RuntimeIdentifier>iossimulator-x64</RuntimeIdentifier>
@@ -16,4 +16,4 @@
     <ProjectReference Include="..\..\src\iOS\Avalonia.iOS\Avalonia.iOS.csproj" />
     <ProjectReference Include="..\ControlCatalog\ControlCatalog.csproj" />
   </ItemGroup>
-</Project>
+</Project>

+ 1 - 5
samples/ControlCatalog.iOS/Info.plist

@@ -13,7 +13,7 @@
 	<key>LSRequiresIPhoneOS</key>
 	<true/>
 	<key>MinimumOSVersion</key>
-	<string>10.0</string>
+	<string>13.0</string>
 	<key>UIDeviceFamily</key>
 	<array>
 		<integer>1</integer>
@@ -39,9 +39,5 @@
 		<string>UIInterfaceOrientationLandscapeLeft</string>
 		<string>UIInterfaceOrientationLandscapeRight</string>
 	</array>
-	<key>UIStatusBarHidden</key>
-	<true/>
-	<key>UIViewControllerBasedStatusBarAppearance</key>
-	<false/>
 </dict>
 </plist>

+ 2 - 2
samples/ControlCatalog/App.xaml.cs

@@ -44,11 +44,11 @@ namespace ControlCatalog
         {
             if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktopLifetime)
             {
-                desktopLifetime.MainWindow = new MainWindow();
+                desktopLifetime.MainWindow = new MainWindow { DataContext = new MainWindowViewModel() };
             }
             else if (ApplicationLifetime is ISingleViewApplicationLifetime singleViewLifetime)
             {
-                singleViewLifetime.MainView = new MainView();
+                singleViewLifetime.MainView = new MainView { DataContext = new MainWindowViewModel() };
             }
 
             base.OnFrameworkInitializationCompleted();

+ 3 - 11
samples/ControlCatalog/ControlCatalog.csproj

@@ -2,7 +2,8 @@
   <PropertyGroup>
     <TargetFrameworks>netstandard2.0;net6.0</TargetFrameworks>
     <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
-    <Nullable>enable</Nullable>    
+    <Nullable>enable</Nullable>
+    <IncludeAvaloniaGenerators>true</IncludeAvaloniaGenerators>
   </PropertyGroup>
   <ItemGroup>
     <Compile Update="**\*.xaml.cs">
@@ -35,14 +36,5 @@
   </ItemGroup>
 
   <Import Project="..\..\build\BuildTargets.targets" />
-
-  <ItemGroup>
-    <None Remove="Pages\CustomDrawing.xaml" />
-  </ItemGroup>
-
-  <ItemGroup>
-    <AvaloniaResource Update="Pages\CustomDrawing.xaml">
-      <Generator></Generator>
-    </AvaloniaResource>
-  </ItemGroup>
+  <Import Project="..\..\build\SourceGenerators.props" />
 </Project>

+ 37 - 1
samples/ControlCatalog/MainView.xaml.cs

@@ -1,5 +1,6 @@
 using System;
 using System.Collections;
+using System.Threading.Tasks;
 using Avalonia;
 using Avalonia.Controls;
 using Avalonia.Controls.ApplicationLifetimes;
@@ -12,6 +13,7 @@ using Avalonia.VisualTree;
 using Avalonia.Styling;
 using ControlCatalog.Models;
 using ControlCatalog.Pages;
+using ControlCatalog.ViewModels;
 
 namespace ControlCatalog
 {
@@ -99,13 +101,47 @@ namespace ControlCatalog
             };
         }
 
+        internal MainWindowViewModel ViewModel => (MainWindowViewModel)DataContext!;
+        
         protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
         {
             base.OnAttachedToVisualTree(e);
+
             var decorations = this.Get<ComboBox>("Decorations");
             if (VisualRoot is Window window)
                 decorations.SelectedIndex = (int)window.SystemDecorations;
-            
+
+            var insets = TopLevel.GetTopLevel(this)!.InsetsManager;
+            if (insets != null)
+            {
+                // In real life application these events should be unsubscribed to avoid memory leaks.
+                ViewModel.SafeAreaPadding = insets.SafeAreaPadding;
+                insets.SafeAreaChanged += (sender, args) =>
+                {
+                    ViewModel.SafeAreaPadding = insets.SafeAreaPadding;
+                };
+
+                ViewModel.DisplayEdgeToEdge = insets.DisplayEdgeToEdge;
+                ViewModel.IsSystemBarVisible = insets.IsSystemBarVisible ?? true;
+
+                ViewModel.PropertyChanged += async (sender, args) =>
+                {
+                    if (args.PropertyName == nameof(ViewModel.DisplayEdgeToEdge))
+                    {
+                        insets.DisplayEdgeToEdge = ViewModel.DisplayEdgeToEdge;
+                    }
+                    else if (args.PropertyName == nameof(ViewModel.IsSystemBarVisible))
+                    {
+                        insets.IsSystemBarVisible = ViewModel.IsSystemBarVisible;
+                    }
+
+                    // Give the OS some time to apply new values and refresh the view model.
+                    await Task.Delay(100);
+                    ViewModel.DisplayEdgeToEdge = insets.DisplayEdgeToEdge;
+                    ViewModel.IsSystemBarVisible = insets.IsSystemBarVisible ?? true;
+                };
+            }
+
             _platformSettings.ColorValuesChanged += PlatformSettingsOnColorValuesChanged;
             PlatformSettingsOnColorValuesChanged(_platformSettings, _platformSettings.GetColorValues());
         }

+ 0 - 1
samples/ControlCatalog/MainWindow.xaml.cs

@@ -17,7 +17,6 @@ namespace ControlCatalog
         {
             this.InitializeComponent();
 
-            DataContext = new MainWindowViewModel();
             _recentMenu = ((NativeMenu.GetMenu(this)?.Items[0] as NativeMenuItem)?.Menu?.Items[2] as NativeMenuItem)?.Menu;
         }
 

+ 1 - 1
samples/ControlCatalog/Pages/ComboBoxPage.xaml.cs

@@ -18,7 +18,7 @@ namespace ControlCatalog.Pages
         {
             AvaloniaXamlLoader.Load(this);
             var fontComboBox = this.Get<ComboBox>("fontComboBox");
-            fontComboBox.ItemsSource = FontManager.Current.GetInstalledFontFamilyNames().Select(x => new FontFamily(x));
+            fontComboBox.Items = FontManager.Current.SystemFonts;
             fontComboBox.SelectedIndex = 0;
         }
     }

+ 3 - 2
samples/ControlCatalog/Pages/ContextFlyoutPage.xaml.cs

@@ -36,8 +36,9 @@ namespace ControlCatalog.Pages
             customContextRequestedBorder.AddHandler(ContextRequestedEvent, CustomContextRequested, RoutingStrategies.Tunnel);
 
             var cancellableContextBorder = this.Get<Border>("CancellableContextBorder");
-            cancellableContextBorder.ContextFlyout!.Closing += ContextFlyoutPage_Closing;
-            cancellableContextBorder.ContextFlyout!.Opening += ContextFlyoutPage_Opening;
+            var flyout = (Flyout)cancellableContextBorder.ContextFlyout!;
+            flyout.Closing += ContextFlyoutPage_Closing;
+            flyout.Opening += ContextFlyoutPage_Opening;
         }
 
         private ContextPageViewModel? _model;

+ 16 - 2
samples/ControlCatalog/Pages/FlyoutsPage.axaml

@@ -16,8 +16,13 @@
             <MenuItem Header="Item 3" />
         </MenuFlyout>
         <Flyout Placement="Bottom" x:Key="BasicFlyout">
+            <Flyout.FlyoutPresenterTheme>
+                <ControlTheme TargetType="FlyoutPresenter" BasedOn="{StaticResource {x:Type FlyoutPresenter}}">
+                    <Setter Property="CornerRadius" Value="20" />
+                </ControlTheme>
+            </Flyout.FlyoutPresenterTheme>
             <Panel Width="100" Height="100">
-                <TextBlock Text="Flyout Content!" />
+                <TextBlock Text="Flyout Content with a custom presenter theme!" TextWrapping="Wrap" />
             </Panel>
         </Flyout>
     </UserControl.Resources>
@@ -136,6 +141,15 @@
                                 </Flyout>
                             </Button.Flyout>
                         </Button>
+                        <Button Content="Placement=Center">
+                            <Button.Flyout>
+                                <Flyout Placement="Center">
+                                    <Panel Width="100" Height="100">
+                                        <TextBlock Text="Flyout Content!" />
+                                    </Panel>
+                                </Flyout>
+                            </Button.Flyout>
+                        </Button>
                         <Button Content="Placement=TopEdgeAlignedLeft">
                             <Button.Flyout>
                                 <Flyout Placement="TopEdgeAlignedLeft">
@@ -190,7 +204,7 @@
                                 </Flyout>
                             </Button.Flyout>
                         </Button>
-                        <Button Content="Placement=RightEdgeAlignedBottom">
+                        <Button Content="Placement=RightEdgeAlignedTop">
                             <Button.Flyout>
                                 <Flyout Placement="RightEdgeAlignedTop">
                                     <Panel Width="100" Height="100">

+ 1 - 7
samples/ControlCatalog/Pages/FlyoutsPage.axaml.cs

@@ -1,11 +1,10 @@
 using Avalonia.Controls;
 using Avalonia.Controls.Primitives;
-using Avalonia.Markup.Xaml;
 using Avalonia.Interactivity;
 
 namespace ControlCatalog.Pages
 {
-    public class FlyoutsPage : UserControl
+    public partial class FlyoutsPage : UserControl
     {
         public FlyoutsPage()
         {
@@ -28,11 +27,6 @@ namespace ControlCatalog.Pages
             }
         }
 
-        private void InitializeComponent()
-        {
-            AvaloniaXamlLoader.Load(this);
-        }
-
         private void SetXamlTexts()
         {
             var bfxt = this.Get<TextBlock>("ButtonFlyoutXamlText");

+ 2 - 9
samples/ControlCatalog/Pages/LabelsPage.axaml.cs

@@ -1,11 +1,9 @@
-using Avalonia;
-using Avalonia.Controls;
-using Avalonia.Markup.Xaml;
+using Avalonia.Controls;
 using ControlCatalog.Models;
 
 namespace ControlCatalog.Pages
 {
-    public class LabelsPage : UserControl
+    public partial class LabelsPage : UserControl
     {
         private Person? _person;
 
@@ -25,11 +23,6 @@ namespace ControlCatalog.Pages
             };
         }
 
-        private void InitializeComponent()
-        {
-            AvaloniaXamlLoader.Load(this);
-        }
-
         public void DoSave()
         {
             

+ 3 - 11
samples/ControlCatalog/Pages/RefreshContainerPage.axaml.cs

@@ -1,18 +1,15 @@
-using System.Threading.Tasks;
-using Avalonia;
-using Avalonia.Controls;
-using Avalonia.Markup.Xaml;
+using Avalonia.Controls;
 using ControlCatalog.ViewModels;
 
 namespace ControlCatalog.Pages
 {
-    public class RefreshContainerPage : UserControl
+    public partial class RefreshContainerPage : UserControl
     {
         private RefreshContainerViewModel _viewModel;
 
         public RefreshContainerPage()
         {
-            this.InitializeComponent();
+            InitializeComponent();
 
             _viewModel = new RefreshContainerViewModel();
 
@@ -27,10 +24,5 @@ namespace ControlCatalog.Pages
 
             deferral.Complete();
         }
-
-        private void InitializeComponent()
-        {
-            AvaloniaXamlLoader.Load(this);
-        }
     }
 }

+ 3 - 10
samples/ControlCatalog/Pages/RelativePanelPage.axaml.cs

@@ -1,19 +1,12 @@
-using Avalonia;
-using Avalonia.Controls;
-using Avalonia.Markup.Xaml;
+using Avalonia.Controls;
 
 namespace ControlCatalog.Pages
 {
-    public class RelativePanelPage : UserControl
+    public partial class RelativePanelPage : UserControl
     {
         public RelativePanelPage()
         {
-            this.InitializeComponent();
-        }
-
-        private void InitializeComponent()
-        {
-            AvaloniaXamlLoader.Load(this);
+            InitializeComponent();
         }
     }
 }

+ 8 - 12
samples/ControlCatalog/Pages/ThemePage.axaml.cs

@@ -1,35 +1,31 @@
-using Avalonia;
-using Avalonia.Controls;
+using Avalonia.Controls;
 using Avalonia.Markup.Xaml;
 using Avalonia.Styling;
 
 namespace ControlCatalog.Pages
 {
-    public class ThemePage : UserControl
+    public partial class ThemePage : UserControl
     {
         public static ThemeVariant Pink { get; } = new("Pink", ThemeVariant.Light);
         
         public ThemePage()
         {
-            AvaloniaXamlLoader.Load(this);
+            InitializeComponent();
 
-            var selector = this.FindControl<ComboBox>("Selector")!;
-            var themeVariantScope = this.FindControl<ThemeVariantScope>("ThemeVariantScope")!;
-
-            selector.Items = new[]
+            Selector.Items = new[]
             {
                 ThemeVariant.Default,
                 ThemeVariant.Dark,
                 ThemeVariant.Light,
                 Pink
             };
-            selector.SelectedIndex = 0;
+            Selector.SelectedIndex = 0;
 
-            selector.SelectionChanged += (_, _) =>
+            Selector.SelectionChanged += (_, _) =>
             {
-                if (selector.SelectedItem is ThemeVariant theme)
+                if (Selector.SelectedItem is ThemeVariant theme)
                 {
-                    themeVariantScope.RequestedThemeVariant = theme;
+                    ThemeVariantScope.RequestedThemeVariant = theme;
                 }
             };
         }

+ 16 - 6
samples/ControlCatalog/Pages/WindowCustomizationsPage.xaml

@@ -5,11 +5,21 @@
              xmlns:viewModels="using:ControlCatalog.ViewModels"
              mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
              x:Class="ControlCatalog.Pages.WindowCustomizationsPage"
-             x:DataType="viewModels:MainWindowViewModel">
-  <StackPanel Spacing="10"  Margin="25">
-    <CheckBox Content="Extend Client Area to Decorations" IsChecked="{Binding ExtendClientAreaEnabled}" />
-    <CheckBox Content="Title Bar" IsChecked="{Binding SystemTitleBarEnabled}" />    
-    <CheckBox Content="Prefer System Chrome" IsChecked="{Binding PreferSystemChromeEnabled}" />
-    <Slider Minimum="-1" Maximum="200" Value="{Binding TitleBarHeight}" />
+             x:DataType="viewModels:MainWindowViewModel"
+             x:CompileBindings="True">
+  <StackPanel>
+    <StackPanel Spacing="10" Margin="25" IsEnabled="{OnFormFactor false, Desktop=true}">
+      <TextBlock Classes="h2" Text="Desktop properties" Margin="4" />
+      <CheckBox Content="Extend Client Area to Decorations" IsChecked="{Binding ExtendClientAreaEnabled}" />
+      <CheckBox Content="Title Bar" IsChecked="{Binding SystemTitleBarEnabled}" />
+      <CheckBox Content="Prefer System Chrome" IsChecked="{Binding PreferSystemChromeEnabled}" />
+      <Slider Minimum="-1" Maximum="200" Value="{Binding TitleBarHeight}" />
+    </StackPanel>
+    <StackPanel Spacing="10" Margin="25" IsEnabled="{OnFormFactor false, Mobile=true}">
+      <TextBlock Classes="h2" Text="Mobile properties" Margin="4" />
+      <CheckBox Content="Is System Bar Visible" IsChecked="{Binding IsSystemBarVisible}" />
+      <CheckBox Content="Display Edge To Edge" IsChecked="{Binding DisplayEdgeToEdge}" />
+      <TextBlock Text="{Binding SafeAreaPadding, StringFormat='Safe Area Padding: {0}'}" />
+    </StackPanel>
   </StackPanel>
 </UserControl>

+ 26 - 4
samples/ControlCatalog/ViewModels/MainWindowViewModel.cs

@@ -6,6 +6,7 @@ using Avalonia.Platform;
 using Avalonia.Reactive;
 using System;
 using System.ComponentModel.DataAnnotations;
+using Avalonia;
 using MiniMvvm;
 
 namespace ControlCatalog.ViewModels
@@ -20,6 +21,9 @@ namespace ControlCatalog.ViewModels
         private bool _systemTitleBarEnabled;
         private bool _preferSystemChromeEnabled;
         private double _titleBarHeight;
+        private bool _isSystemBarVisible;
+        private bool _displayEdgeToEdge;
+        private Thickness _safeAreaPadding;
 
         public MainWindowViewModel()
         {
@@ -78,25 +82,25 @@ namespace ControlCatalog.ViewModels
         {
             get { return _chromeHints; }
             set { this.RaiseAndSetIfChanged(ref _chromeHints, value); }
-        }        
+        }
 
         public bool ExtendClientAreaEnabled
         {
             get { return _extendClientAreaEnabled; }
             set { this.RaiseAndSetIfChanged(ref _extendClientAreaEnabled, value); }
-        }        
+        }
 
         public bool SystemTitleBarEnabled
         {
             get { return _systemTitleBarEnabled; }
             set { this.RaiseAndSetIfChanged(ref _systemTitleBarEnabled, value); }
-        }        
+        }
 
         public bool PreferSystemChromeEnabled
         {
             get { return _preferSystemChromeEnabled; }
             set { this.RaiseAndSetIfChanged(ref _preferSystemChromeEnabled, value); }
-        }        
+        }
 
         public double TitleBarHeight
         {
@@ -122,6 +126,24 @@ namespace ControlCatalog.ViewModels
             set { this.RaiseAndSetIfChanged(ref _isMenuItemChecked, value); }
         }
 
+        public bool IsSystemBarVisible
+        {
+            get { return _isSystemBarVisible; }
+            set { this.RaiseAndSetIfChanged(ref _isSystemBarVisible, value); }
+        }
+
+        public bool DisplayEdgeToEdge
+        {
+            get { return _displayEdgeToEdge; }
+            set { this.RaiseAndSetIfChanged(ref _displayEdgeToEdge, value); }
+        }
+        
+        public Thickness SafeAreaPadding
+        {
+            get { return _safeAreaPadding; }
+            set { this.RaiseAndSetIfChanged(ref _safeAreaPadding, value); }
+        }
+
         public MiniCommand AboutCommand { get; }
 
         public MiniCommand ExitCommand { get; }

+ 7 - 0
samples/Generators.Sandbox/App.xaml

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

+ 20 - 0
samples/Generators.Sandbox/App.xaml.cs

@@ -0,0 +1,20 @@
+using Avalonia;
+using Avalonia.Markup.Xaml;
+using Generators.Sandbox.ViewModels;
+
+namespace Generators.Sandbox;
+
+public class App : Application
+{
+    public override void Initialize() => AvaloniaXamlLoader.Load(this);
+
+    public override void OnFrameworkInitializationCompleted()
+    {
+        var view = new Views.SignUpView
+        {
+            ViewModel = new SignUpViewModel()
+        };
+        view.Show();
+        base.OnFrameworkInitializationCompleted();
+    }
+}

+ 10 - 0
samples/Generators.Sandbox/Controls/CustomTextBox.cs

@@ -0,0 +1,10 @@
+using System;
+using Avalonia.Controls;
+using Avalonia.Styling;
+
+namespace Generators.Sandbox.Controls;
+
+public class CustomTextBox : TextBox, IStyleable
+{
+    Type IStyleable.StyleKey => typeof(TextBox);
+}

+ 45 - 0
samples/Generators.Sandbox/Controls/SignUpView.xaml

@@ -0,0 +1,45 @@
+<UserControl xmlns="https://github.com/avaloniaui"
+        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+        xmlns:controls="using:Generators.Sandbox.Controls"
+        x:Class="Generators.Sandbox.Controls.SignUpView">
+    <StackPanel>
+        <controls:CustomTextBox Margin="0 10 0 0"
+                 x:Name="UserNameTextBox"
+                 Watermark="Please, enter user name..."
+                 UseFloatingWatermark="True" />
+        <TextBlock x:Name="UserNameValidation" 
+                   Foreground="Red"
+                   FontSize="12" />
+        <TextBox Margin="0 10 0 0"
+                 x:Name="PasswordTextBox"
+                 Watermark="Please, enter your password..."
+                 UseFloatingWatermark="True"
+                 PasswordChar="*" />
+        <TextBlock x:Name="PasswordValidation" 
+                   Foreground="Red"
+                   FontSize="12" />
+        <TextBox Margin="0 10 0 0"
+                 x:Name="ConfirmPasswordTextBox"
+                 Watermark="Please, confirm the password..."
+                 UseFloatingWatermark="True"
+                 PasswordChar="*" />
+        <TextBlock x:Name="ConfirmPasswordValidation"
+                   TextWrapping="Wrap"
+                   Foreground="Red"
+                   FontSize="12" />
+        <TextBlock>
+            <TextBlock.Inlines>
+                <InlineCollection>
+                    <Run x:Name="SignUpButtonDescription" />
+                </InlineCollection>
+            </TextBlock.Inlines>
+        </TextBlock>
+        <Button Margin="0 10 0 5"
+                Content="Sign up"
+                x:Name="SignUpButton" />
+        <TextBlock x:Name="CompoundValidation"
+                   TextWrapping="Wrap"
+                   Foreground="Red"
+                   FontSize="12" />
+    </StackPanel>
+</UserControl>

+ 54 - 0
samples/Generators.Sandbox/Controls/SignUpView.xaml.cs

@@ -0,0 +1,54 @@
+using System;
+using System.Reactive.Disposables;
+using Avalonia.ReactiveUI;
+using Generators.Sandbox.ViewModels;
+using ReactiveUI;
+using ReactiveUI.Validation.Extensions;
+using ReactiveUI.Validation.Formatters;
+
+namespace Generators.Sandbox.Controls;
+
+/// <summary>
+/// This is a sample view class with typed x:Name references generated using
+/// .NET 5 source generators. The class has to be partial because x:Name
+/// references are living in a separate partial class file. See also:
+/// https://devblogs.microsoft.com/dotnet/new-c-source-generator-samples/
+/// </summary>
+public partial class SignUpView : ReactiveUserControl<SignUpViewModel>
+{
+    public SignUpView()
+    {
+        // The InitializeComponent method is also generated automatically
+        // and lives in the autogenerated part of the partial class.
+        InitializeComponent();
+        this.WhenActivated(disposables =>
+        {
+            this.Bind(ViewModel, x => x.UserName, x => x.UserNameTextBox.Text)
+                .DisposeWith(disposables);
+            this.Bind(ViewModel, x => x.Password, x => x.PasswordTextBox.Text)
+                .DisposeWith(disposables);
+            this.Bind(ViewModel, x => x.ConfirmPassword, x => x.ConfirmPasswordTextBox.Text)
+                .DisposeWith(disposables);
+            this.BindCommand(ViewModel, x => x.SignUp, x => x.SignUpButton)
+                .DisposeWith(disposables);
+
+            this.BindValidation(ViewModel, x => x.UserName, x => x.UserNameValidation.Text)
+                .DisposeWith(disposables);
+            this.BindValidation(ViewModel, x => x.Password, x => x.PasswordValidation.Text)
+                .DisposeWith(disposables);
+            this.BindValidation(ViewModel, x => x.ConfirmPassword, x => x.ConfirmPasswordValidation.Text)
+                .DisposeWith(disposables);
+
+            var newLineFormatter = new SingleLineFormatter(Environment.NewLine);
+            this.BindValidation(ViewModel, x => x.CompoundValidation.Text, newLineFormatter)
+                .DisposeWith(disposables);
+
+            // The references to text boxes below are also auto generated.
+            // Use Ctrl+Click in order to view the generated sources. 
+            UserNameTextBox.Text = "Joseph!";
+            PasswordTextBox.Text = "1234";
+            ConfirmPasswordTextBox.Text = "1234";
+            SignUpButtonDescription.Text = "Press the button below to sign up.";
+        });
+    }
+}

+ 28 - 0
samples/Generators.Sandbox/Generators.Sandbox.csproj

@@ -0,0 +1,28 @@
+<Project Sdk="Microsoft.NET.Sdk">
+  <PropertyGroup>
+    <OutputType>Exe</OutputType>
+    <TargetFramework>net6.0</TargetFramework>
+    <IncludeAvaloniaGenerators>true</IncludeAvaloniaGenerators>
+  </PropertyGroup>
+
+  <ItemGroup>
+    <AvaloniaResource Include="**\*.xaml"/>
+    <!-- Note this AdditionalFiles directive. -->
+    <AdditionalFiles Include="**\*.xaml"/>
+  </ItemGroup>
+
+  <ItemGroup>
+    <PackageReference Include="ReactiveUI.Validation" Version="3.0.22"/>
+  </ItemGroup>
+
+  <ItemGroup>
+    <ProjectReference Include="..\..\packages\Avalonia\Avalonia.csproj"/>
+    <ProjectReference Include="..\..\src\Avalonia.Desktop\Avalonia.Desktop.csproj"/>
+    <ProjectReference Include="..\..\src\Avalonia.ReactiveUI\Avalonia.ReactiveUI.csproj"/>
+    <ProjectReference Include="..\..\src\Avalonia.Themes.Fluent\Avalonia.Themes.Fluent.csproj"/>
+    <ProjectReference Include="..\..\src\Avalonia.Fonts.Inter\Avalonia.Fonts.Inter.csproj"/>
+  </ItemGroup>
+
+  <Import Project="..\..\build\BuildTargets.targets"/>
+  <Import Project="..\..\build\SourceGenerators.props"/>
+</Project>

+ 15 - 0
samples/Generators.Sandbox/Program.cs

@@ -0,0 +1,15 @@
+using Avalonia;
+using Avalonia.ReactiveUI;
+
+namespace Generators.Sandbox;
+
+internal static class Program
+{
+    public static void Main(string[] args) => BuildAvaloniaApp().StartWithClassicDesktopLifetime(args);
+
+    private static AppBuilder BuildAvaloniaApp()
+        => AppBuilder.Configure<App>()
+            .UseReactiveUI()
+            .UsePlatformDetect()
+            .LogToTrace();
+}

+ 70 - 0
samples/Generators.Sandbox/ViewModels/SignUpViewModel.cs

@@ -0,0 +1,70 @@
+using System.Reactive;
+using ReactiveUI;
+using ReactiveUI.Validation.Extensions;
+using ReactiveUI.Validation.Helpers;
+
+namespace Generators.Sandbox.ViewModels;
+
+public class SignUpViewModel : ReactiveValidationObject
+{
+    private string _userName = string.Empty;
+    private string _password = string.Empty;
+    private string _confirmPassword = string.Empty;
+
+    public SignUpViewModel()
+    {
+        this.ValidationRule(
+            vm => vm.UserName,
+            name => !string.IsNullOrWhiteSpace(name),
+            "UserName is required.");
+
+        this.ValidationRule(
+            vm => vm.Password,
+            password => !string.IsNullOrWhiteSpace(password),
+            "Password is required.");
+
+        this.ValidationRule(
+            vm => vm.Password,
+            password => password?.Length > 2,
+            password => $"Password should be longer, current length: {password.Length}");
+
+        this.ValidationRule(
+            vm => vm.ConfirmPassword,
+            confirmation => !string.IsNullOrWhiteSpace(confirmation),
+            "Confirm password field is required.");
+
+        var passwordsObservable =
+            this.WhenAnyValue(
+                x => x.Password,
+                x => x.ConfirmPassword,
+                (password, confirmation) =>
+                    password == confirmation);
+
+        this.ValidationRule(
+            vm => vm.ConfirmPassword,
+            passwordsObservable,
+            "Passwords must match.");
+
+        SignUp = ReactiveCommand.Create(() => {}, this.IsValid());
+    }
+
+    public ReactiveCommand<Unit, Unit> SignUp { get; }
+
+    public string UserName
+    {
+        get => _userName;
+        set => this.RaiseAndSetIfChanged(ref _userName, value);
+    }
+
+    public string Password
+    {
+        get => _password;
+        set => this.RaiseAndSetIfChanged(ref _password, value);
+    }
+
+    public string ConfirmPassword
+    {
+        get => _confirmPassword;
+        set => this.RaiseAndSetIfChanged(ref _confirmPassword, value);
+    }
+}

+ 9 - 0
samples/Generators.Sandbox/Views/SignUpView.xaml

@@ -0,0 +1,9 @@
+<Window xmlns="https://github.com/avaloniaui"
+        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+        xmlns:controls="using:Generators.Sandbox.Controls"
+        x:Class="Generators.Sandbox.Views.SignUpView">
+    <StackPanel Margin="10">
+        <TextBlock Text="Sign Up" />
+        <controls:SignUpView x:Name="SignUpControl" />
+    </StackPanel>
+</Window>

+ 28 - 0
samples/Generators.Sandbox/Views/SignUpView.xaml.cs

@@ -0,0 +1,28 @@
+using System.Reactive.Disposables;
+using Avalonia.ReactiveUI;
+using Generators.Sandbox.ViewModels;
+using ReactiveUI;
+
+namespace Generators.Sandbox.Views;
+
+/// <summary>
+/// This is a sample view class with typed x:Name references generated using
+/// .NET 5 source generators. The class has to be partial because x:Name
+/// references are living in a separate partial class file. See also:
+/// https://devblogs.microsoft.com/dotnet/new-c-source-generator-samples/
+/// </summary>
+public partial class SignUpView : ReactiveWindow<SignUpViewModel>
+{
+    public SignUpView()
+    {
+        // The InitializeComponent method is also generated automatically
+        // and lives in the autogenerated part of the partial class.
+        InitializeComponent();
+        this.WhenActivated(disposables =>
+        {
+            this.WhenAnyValue(view => view.ViewModel)
+                .BindTo(this, view => view.SignUpControl.ViewModel)
+                .DisposeWith(disposables);
+        });
+    }
+}

+ 1 - 0
samples/GpuInterop/GpuInterop.csproj

@@ -15,6 +15,7 @@
     <ProjectReference Include="..\..\src\Avalonia.Diagnostics\Avalonia.Diagnostics.csproj" />
     <ProjectReference Include="..\..\src\Avalonia.Controls.ColorPicker\Avalonia.Controls.ColorPicker.csproj" />
     <ProjectReference Include="..\..\src\Avalonia.Controls.DataGrid\Avalonia.Controls.DataGrid.csproj" />
+    <ProjectReference Include="..\..\src\Avalonia.Fonts.Inter\Avalonia.Fonts.Inter.csproj" />
     <ProjectReference Include="..\..\src\Avalonia.Themes.Fluent\Avalonia.Themes.Fluent.csproj" />
   </ItemGroup>
 

+ 41 - 34
samples/IntegrationTestApp/ShowWindowTest.axaml

@@ -1,41 +1,48 @@
 <Window xmlns="https://github.com/avaloniaui"
         xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+        xmlns:integrationTestApp="clr-namespace:IntegrationTestApp"
         x:Class="IntegrationTestApp.ShowWindowTest"
         Name="SecondaryWindow"
         x:DataType="Window"
         Title="Show Window Test">
-  <Grid ColumnDefinitions="Auto,Auto" RowDefinitions="Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto">
-    <Label Grid.Column="0" Grid.Row="1">Client Size</Label>
-    <TextBox Name="ClientSize" Grid.Column="1" Grid.Row="1" IsReadOnly="True"
-             Text="{Binding ClientSize, Mode=OneWay}"/>
-    
-    <Label Grid.Column="0" Grid.Row="2">Frame Size</Label>
-    <TextBox Name="FrameSize" Grid.Column="1" Grid.Row="2" IsReadOnly="True"
-             Text="{Binding FrameSize, Mode=OneWay}"/>
-
-    <Label Grid.Column="0" Grid.Row="3">Position</Label>
-    <TextBox Name="Position" Grid.Column="1" Grid.Row="3" IsReadOnly="True"/>
-
-    <Label Grid.Column="0" Grid.Row="4">Owner Rect</Label>
-    <TextBox Name="OwnerRect" Grid.Column="1" Grid.Row="4" IsReadOnly="True"/>
-    
-    <Label Grid.Column="0" Grid.Row="5">Screen Rect</Label>
-    <TextBox Name="ScreenRect" Grid.Column="1" Grid.Row="5" IsReadOnly="True"/>
-
-    <Label Grid.Column="0" Grid.Row="6">Scaling</Label>
-    <TextBox Name="Scaling" Grid.Column="1" Grid.Row="6" IsReadOnly="True"/>
-    
-    <Label Grid.Column="0" Grid.Row="7">WindowState</Label>
-    <ComboBox Name="WindowState" Grid.Column="1" Grid.Row="7" SelectedIndex="{Binding WindowState}">
-      <ComboBoxItem Name="WindowStateNormal">Normal</ComboBoxItem>
-      <ComboBoxItem Name="WindowStateMinimized">Minimized</ComboBoxItem>
-      <ComboBoxItem Name="WindowStateMaximized">Maximized</ComboBoxItem>
-      <ComboBoxItem Name="WindowStateFullScreen">FullScreen</ComboBoxItem>
-    </ComboBox>
-
-    <Label Grid.Column="0" Grid.Row="8">Order (mac)</Label>
-    <TextBox Name="Order" Grid.Column="1" Grid.Row="8" IsReadOnly="True"/>
-
-    <Button Name="HideButton" Grid.Row="9" Command="{Binding $parent[Window].Hide}">Hide</Button>
-  </Grid>
+  <integrationTestApp:MeasureBorder Name="MyBorder">
+    <Grid ColumnDefinitions="Auto,Auto" RowDefinitions="Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto">
+      <Label Grid.Column="0" Grid.Row="1">Client Size</Label>
+      <TextBox Name="CurrentClientSize" Grid.Column="1" Grid.Row="1" IsReadOnly="True"
+               Text="{Binding ClientSize, Mode=OneWay}" />
+
+      <Label Grid.Column="0" Grid.Row="2">Frame Size</Label>
+      <TextBox Name="CurrentFrameSize" Grid.Column="1" Grid.Row="2" IsReadOnly="True"
+               Text="{Binding FrameSize, Mode=OneWay}" />
+
+      <Label Grid.Column="0" Grid.Row="3">Position</Label>
+      <TextBox Name="CurrentPosition" Grid.Column="1" Grid.Row="3" IsReadOnly="True" />
+
+      <Label Grid.Column="0" Grid.Row="4">Owner Rect</Label>
+      <TextBox Name="CurrentOwnerRect" Grid.Column="1" Grid.Row="4" IsReadOnly="True" />
+
+      <Label Grid.Column="0" Grid.Row="5">Screen Rect</Label>
+      <TextBox Name="CurrentScreenRect" Grid.Column="1" Grid.Row="5" IsReadOnly="True" />
+
+      <Label Grid.Column="0" Grid.Row="6">Scaling</Label>
+      <TextBox Name="CurrentScaling" Grid.Column="1" Grid.Row="6" IsReadOnly="True" />
+
+      <Label Grid.Column="0" Grid.Row="7">WindowState</Label>
+      <ComboBox Name="CurrentWindowState" Grid.Column="1" Grid.Row="7" SelectedIndex="{Binding WindowState}">
+        <ComboBoxItem Name="WindowStateNormal">Normal</ComboBoxItem>
+        <ComboBoxItem Name="WindowStateMinimized">Minimized</ComboBoxItem>
+        <ComboBoxItem Name="WindowStateMaximized">Maximized</ComboBoxItem>
+        <ComboBoxItem Name="WindowStateFullScreen">FullScreen</ComboBoxItem>
+      </ComboBox>
+
+      <Label Grid.Column="0" Grid.Row="8">Order (mac)</Label>
+      <TextBox Name="CurrentOrder" Grid.Column="1" Grid.Row="8" IsReadOnly="True" />
+      
+      <Label Grid.Row="9" Content="MeasuredWith:" />
+      <TextBlock Grid.Column="1" Grid.Row="9" Name="CurrentMeasuredWithText" Text="{Binding #MyBorder.MeasuredWith}" />
+
+      <Button Name="HideButton" Grid.Row="10" Command="{Binding $parent[Window].Hide}">Hide</Button>
+      
+    </Grid>
+  </integrationTestApp:MeasureBorder>
 </Window>

+ 25 - 6
samples/IntegrationTestApp/ShowWindowTest.axaml.cs

@@ -7,6 +7,25 @@ using Avalonia.Threading;
 
 namespace IntegrationTestApp
 {
+    public class MeasureBorder : Border
+    {
+        protected override Size MeasureOverride(Size availableSize)
+        {
+            MeasuredWith = availableSize;
+            
+            return base.MeasureOverride(availableSize);
+        }
+
+        public static readonly StyledProperty<Size> MeasuredWithProperty = AvaloniaProperty.Register<MeasureBorder, Size>(
+            nameof(MeasuredWith));
+
+        public Size MeasuredWith
+        {
+            get => GetValue(MeasuredWithProperty);
+            set => SetValue(MeasuredWithProperty, value);
+        }
+    }
+    
     public class ShowWindowTest : Window
     {
         private readonly DispatcherTimer? _timer;
@@ -16,11 +35,11 @@ namespace IntegrationTestApp
         {
             InitializeComponent();
             DataContext = this;
-            PositionChanged += (s, e) => this.GetControl<TextBox>("Position").Text = $"{Position}";
+            PositionChanged += (s, e) => this.GetControl<TextBox>("CurrentPosition").Text = $"{Position}";
 
             if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
             {
-                _orderTextBox = this.GetControl<TextBox>("Order");
+                _orderTextBox = this.GetControl<TextBox>("CurrentOrder");
                 _timer = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(250) };
                 _timer.Tick += TimerOnTick;
                 _timer.Start();
@@ -36,13 +55,13 @@ namespace IntegrationTestApp
         {
             base.OnOpened(e);
             var scaling = PlatformImpl!.DesktopScaling;
-            this.GetControl<TextBox>("Position").Text = $"{Position}";
-            this.GetControl<TextBox>("ScreenRect").Text = $"{Screens.ScreenFromVisual(this)?.WorkingArea}";
-            this.GetControl<TextBox>("Scaling").Text = $"{scaling}";
+            this.GetControl<TextBox>("CurrentPosition").Text = $"{Position}";
+            this.GetControl<TextBox>("CurrentScreenRect").Text = $"{Screens.ScreenFromVisual(this)?.WorkingArea}";
+            this.GetControl<TextBox>("CurrentScaling").Text = $"{scaling}";
 
             if (Owner is not null)
             {
-                var ownerRect = this.GetControl<TextBox>("OwnerRect");
+                var ownerRect = this.GetControl<TextBox>("CurrentOwnerRect");
                 var owner = (Window)Owner;
                 ownerRect.Text = $"{owner.Position}, {PixelSize.FromSize(owner.FrameSize!.Value, scaling)}";
             }

+ 9 - 2
samples/IntegrationTestApp/bundle.sh

@@ -1,5 +1,12 @@
 #!/usr/bin/env bash
 
 cd $(cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd)
-dotnet restore -r osx-arm64
-dotnet msbuild -t:BundleApp -p:RuntimeIdentifier=osx-arm64 -p:_AvaloniaUseExternalMSBuild=false
+
+arch="x64"
+
+if [[ $(uname -m) == 'arm64' ]]; then
+arch="arm64"
+fi
+
+dotnet restore -r osx-$arch
+dotnet msbuild -t:BundleApp -p:RuntimeIdentifier=osx-$arch -p:_AvaloniaUseExternalMSBuild=false

+ 1 - 0
samples/MobileSandbox/MobileSandbox.csproj

@@ -28,6 +28,7 @@
     <ProjectReference Include="..\..\packages\Avalonia\Avalonia.csproj" />
     <ProjectReference Include="..\..\src\Avalonia.Controls.ColorPicker\Avalonia.Controls.ColorPicker.csproj" />
     <ProjectReference Include="..\..\src\Avalonia.Controls.DataGrid\Avalonia.Controls.DataGrid.csproj" />
+    <ProjectReference Include="..\..\src\Avalonia.Fonts.Inter\Avalonia.Fonts.Inter.csproj" />
     <ProjectReference Include="..\..\src\Avalonia.Themes.Fluent\Avalonia.Themes.Fluent.csproj" />
     <ProjectReference Include="..\MiniMvvm\MiniMvvm.csproj" />
     <ProjectReference Include="..\SampleControls\ControlSamples.csproj" />

+ 1 - 0
samples/PlatformSanityChecks/PlatformSanityChecks.csproj

@@ -7,6 +7,7 @@
 
   <ItemGroup>
     <ProjectReference Include="..\..\src\Avalonia.Desktop\Avalonia.Desktop.csproj" />
+    <ProjectReference Include="..\..\src\Avalonia.Fonts.Inter\Avalonia.Fonts.Inter.csproj" />
     <ProjectReference Include="..\..\src\Avalonia.Themes.Simple\Avalonia.Themes.Simple.csproj" />
     <ProjectReference Include="..\..\src\Avalonia.X11\Avalonia.X11.csproj" />
   </ItemGroup>

+ 1 - 0
samples/Previewer/Previewer.csproj

@@ -10,6 +10,7 @@
     <EmbeddedResource Include="**\*.xaml" />
   </ItemGroup>
   <ItemGroup>
+    <ProjectReference Include="..\..\src\Avalonia.Fonts.Inter\Avalonia.Fonts.Inter.csproj" />
     <ProjectReference Include="..\..\src\Avalonia.Themes.Simple\Avalonia.Themes.Simple.csproj" />
   </ItemGroup>
 

+ 1 - 0
samples/ReactiveUIDemo/ReactiveUIDemo.csproj

@@ -7,6 +7,7 @@
   
   <ItemGroup>
     <ProjectReference Include="..\..\src\Avalonia.Diagnostics\Avalonia.Diagnostics.csproj" />
+    <ProjectReference Include="..\..\src\Avalonia.Fonts.Inter\Avalonia.Fonts.Inter.csproj" />
     <ProjectReference Include="..\..\src\Avalonia.ReactiveUI\Avalonia.ReactiveUI.csproj" />
     <ProjectReference Include="..\..\src\Avalonia.Themes.Fluent\Avalonia.Themes.Fluent.csproj" />
   </ItemGroup>

+ 1 - 0
samples/RenderDemo/RenderDemo.csproj

@@ -12,6 +12,7 @@
   </ItemGroup>
   <ItemGroup>
     <ProjectReference Include="..\..\src\Avalonia.Diagnostics\Avalonia.Diagnostics.csproj" />
+    <ProjectReference Include="..\..\src\Avalonia.Fonts.Inter\Avalonia.Fonts.Inter.csproj" />
     <ProjectReference Include="..\..\src\Avalonia.Themes.Fluent\Avalonia.Themes.Fluent.csproj" />
     <ProjectReference Include="..\..\src\Linux\Avalonia.LinuxFramebuffer\Avalonia.LinuxFramebuffer.csproj" />
     <ProjectReference Include="..\MiniMvvm\MiniMvvm.csproj" />

+ 1 - 0
samples/Sandbox/Sandbox.csproj

@@ -10,6 +10,7 @@
     <ProjectReference Include="..\..\src\Avalonia.Diagnostics\Avalonia.Diagnostics.csproj" />
     <ProjectReference Include="..\..\src\Avalonia.Controls.ColorPicker\Avalonia.Controls.ColorPicker.csproj" />
     <ProjectReference Include="..\..\src\Avalonia.Controls.DataGrid\Avalonia.Controls.DataGrid.csproj" />
+    <ProjectReference Include="..\..\src\Avalonia.Fonts.Inter\Avalonia.Fonts.Inter.csproj" />
     <ProjectReference Include="..\..\src\Avalonia.Themes.Fluent\Avalonia.Themes.Fluent.csproj" />
   </ItemGroup>
   

+ 1 - 0
samples/VirtualizationDemo/VirtualizationDemo.csproj

@@ -5,6 +5,7 @@
   </PropertyGroup>
   <ItemGroup>
     <ProjectReference Include="..\..\src\Avalonia.Diagnostics\Avalonia.Diagnostics.csproj" />
+    <ProjectReference Include="..\..\src\Avalonia.Fonts.Inter\Avalonia.Fonts.Inter.csproj" />
     <ProjectReference Include="..\..\src\Avalonia.Themes.Simple\Avalonia.Themes.Simple.csproj" />
     <ProjectReference Include="..\..\src\Linux\Avalonia.LinuxFramebuffer\Avalonia.LinuxFramebuffer.csproj" />
     <ProjectReference Include="..\MiniMvvm\MiniMvvm.csproj" />

+ 15 - 0
src/Android/Avalonia.Android/AvaloniaMainActivity.cs

@@ -1,4 +1,5 @@
 using System;
+using System.Diagnostics;
 using Android.App;
 using Android.Content;
 using Android.Content.PM;
@@ -32,6 +33,9 @@ namespace Avalonia.Android
                 lifetime.View = View;
             }
 
+            Window?.ClearFlags(WindowManagerFlags.TranslucentStatus);
+            Window?.AddFlags(WindowManagerFlags.DrawsSystemBarBackgrounds);
+
             base.OnCreate(savedInstanceState);
 
             SetContentView(View);
@@ -55,6 +59,17 @@ namespace Avalonia.Android
             }
         }
 
+        protected override void OnResume()
+        {
+            base.OnResume();
+
+            // Android only respects LayoutInDisplayCutoutMode value if it has been set once before window becomes visible.
+            if (Build.VERSION.SdkInt >= BuildVersionCodes.P)
+            {
+                Window.Attributes.LayoutInDisplayCutoutMode = LayoutInDisplayCutoutMode.ShortEdges;
+            }
+        }
+
         public event EventHandler<AndroidBackRequestedEventArgs> BackRequested;
 
         public override void OnBackPressed()

+ 6 - 0
src/Android/Avalonia.Android/AvaloniaView.cs

@@ -8,6 +8,7 @@ using Avalonia.Android.Platform;
 using Avalonia.Android.Platform.SkiaPlatform;
 using Avalonia.Controls;
 using Avalonia.Controls.Embedding;
+using Avalonia.Controls.Platform;
 using Avalonia.Platform;
 using Avalonia.Rendering;
 
@@ -67,6 +68,11 @@ namespace Avalonia.Android
                 }
 
                 _root.Renderer.Start();
+
+                if (_view.TryGetFeature<IInsetsManager>(out var insetsManager) == true)
+                {
+                    (insetsManager as AndroidInsetsManager)?.ApplyStatusBarState();
+                }
             }
             else
             {

+ 235 - 0
src/Android/Avalonia.Android/Platform/AndroidInsetsManager.cs

@@ -0,0 +1,235 @@
+using System;
+using System.Collections.Generic;
+using Android.OS;
+using Android.Views;
+using AndroidX.AppCompat.App;
+using AndroidX.Core.View;
+using Avalonia.Android.Platform.SkiaPlatform;
+using Avalonia.Controls.Platform;
+using static Avalonia.Controls.Platform.IInsetsManager;
+
+namespace Avalonia.Android.Platform
+{
+    internal class AndroidInsetsManager : Java.Lang.Object, IInsetsManager, IOnApplyWindowInsetsListener, ViewTreeObserver.IOnGlobalLayoutListener
+    {
+        private readonly AvaloniaMainActivity _activity;
+        private readonly TopLevelImpl _topLevel;
+        private readonly InsetsAnimationCallback _callback;
+        private bool _displayEdgeToEdge;
+        private bool _usesLegacyLayouts;
+        private bool? _systemUiVisibility;
+        private SystemBarTheme? _statusBarTheme;
+        private bool? _isDefaultSystemBarLightTheme;
+
+        public event EventHandler<SafeAreaChangedArgs> SafeAreaChanged;
+
+        public bool DisplayEdgeToEdge
+        {
+            get => _displayEdgeToEdge; 
+            set
+            {
+                _displayEdgeToEdge = value;
+
+                if(Build.VERSION.SdkInt >= BuildVersionCodes.P)
+                {
+                    _activity.Window.Attributes.LayoutInDisplayCutoutMode = value ? LayoutInDisplayCutoutMode.ShortEdges : LayoutInDisplayCutoutMode.Default;
+                }
+
+                WindowCompat.SetDecorFitsSystemWindows(_activity.Window, !value);
+            }
+        }
+
+        public AndroidInsetsManager(AvaloniaMainActivity activity, TopLevelImpl topLevel)
+        {
+            _activity = activity;
+            _topLevel = topLevel;
+            _callback = new InsetsAnimationCallback(WindowInsetsAnimationCompat.Callback.DispatchModeStop);
+
+            _callback.InsetsManager = this;
+
+            ViewCompat.SetOnApplyWindowInsetsListener(_activity.Window.DecorView, this);
+
+            ViewCompat.SetWindowInsetsAnimationCallback(_activity.Window.DecorView, _callback);
+
+            if(Build.VERSION.SdkInt < BuildVersionCodes.R)
+            {
+                _usesLegacyLayouts = true;
+                _activity.Window.DecorView.ViewTreeObserver.AddOnGlobalLayoutListener(this);
+            }
+
+            DisplayEdgeToEdge = false;
+        }
+
+        public Thickness SafeAreaPadding
+        {
+            get
+            {
+                var insets = ViewCompat.GetRootWindowInsets(_activity.Window.DecorView);
+
+                if (insets != null)
+                {
+                    var renderScaling = _topLevel.RenderScaling;
+
+                    var inset = insets.GetInsets(
+                        (DisplayEdgeToEdge ?
+                            WindowInsetsCompat.Type.StatusBars() | WindowInsetsCompat.Type.NavigationBars() |
+                            WindowInsetsCompat.Type.DisplayCutout() :
+                            0) | WindowInsetsCompat.Type.Ime());
+                    var navBarInset = insets.GetInsets(WindowInsetsCompat.Type.NavigationBars());
+                    var imeInset = insets.GetInsets(WindowInsetsCompat.Type.Ime());
+
+                    return new Thickness(inset.Left / renderScaling,
+                        inset.Top / renderScaling,
+                        inset.Right / renderScaling,
+                        (imeInset.Bottom > 0 && ((_usesLegacyLayouts && !DisplayEdgeToEdge) || !_usesLegacyLayouts) ?
+                            imeInset.Bottom - navBarInset.Bottom :
+                            inset.Bottom) / renderScaling);
+                }
+
+                return default;
+            }
+        }
+
+        public WindowInsetsCompat OnApplyWindowInsets(View v, WindowInsetsCompat insets)
+        {
+            NotifySafeAreaChanged(SafeAreaPadding);
+            return insets;
+        }
+
+        private void NotifySafeAreaChanged(Thickness safeAreaPadding)
+        {
+            SafeAreaChanged?.Invoke(this, new SafeAreaChangedArgs(safeAreaPadding));
+        }
+
+        public void OnGlobalLayout()
+        {
+            NotifySafeAreaChanged(SafeAreaPadding);
+        }
+
+        public SystemBarTheme? SystemBarTheme
+        {
+            get
+            {
+                try
+                {
+                    var compat = new WindowInsetsControllerCompat(_activity.Window, _topLevel.View);
+
+                    return compat.AppearanceLightStatusBars ? Controls.Platform.SystemBarTheme.Light : Controls.Platform.SystemBarTheme.Dark;
+                }
+                catch (Exception _)
+                {
+                    return Controls.Platform.SystemBarTheme.Light;
+                }
+            }
+            set
+            {
+                _statusBarTheme = value;
+
+                var isDefault = _statusBarTheme == null;
+
+                if (!_topLevel.View.IsShown)
+                {
+                    return;
+                }
+
+                var compat = new WindowInsetsControllerCompat(_activity.Window, _topLevel.View);
+
+                if (_isDefaultSystemBarLightTheme == null)
+                {
+                    _isDefaultSystemBarLightTheme = compat.AppearanceLightStatusBars;
+                }
+
+                if (value == null && _isDefaultSystemBarLightTheme != null)
+                {
+                    value = (bool)_isDefaultSystemBarLightTheme ? Controls.Platform.SystemBarTheme.Light : Controls.Platform.SystemBarTheme.Dark;
+                }
+
+                compat.AppearanceLightStatusBars = value == Controls.Platform.SystemBarTheme.Light;
+                compat.AppearanceLightNavigationBars = value == Controls.Platform.SystemBarTheme.Light;
+
+                AppCompatDelegate.DefaultNightMode = isDefault ? AppCompatDelegate.ModeNightFollowSystem : compat.AppearanceLightStatusBars ? AppCompatDelegate.ModeNightNo : AppCompatDelegate.ModeNightYes;
+            }
+        }
+
+        public bool? IsSystemBarVisible
+        {
+            get
+            {
+                if(_activity.Window == null)
+                {
+                    return true;
+                }
+                var compat = ViewCompat.GetRootWindowInsets(_activity.Window.DecorView);
+
+                return compat?.IsVisible(WindowInsetsCompat.Type.StatusBars() | WindowInsetsCompat.Type.NavigationBars());
+            }
+            set
+            {
+                _systemUiVisibility = value;
+
+                if (!_topLevel.View.IsShown)
+                {
+                    return;
+                }
+
+                var compat = WindowCompat.GetInsetsController(_activity.Window, _topLevel.View);
+
+                if (value == null || value.Value)
+                {
+                    compat?.Show(WindowInsetsCompat.Type.StatusBars() | WindowInsetsCompat.Type.NavigationBars());
+                }
+                else
+                {
+                    compat?.Hide(WindowInsetsCompat.Type.StatusBars() | WindowInsetsCompat.Type.NavigationBars());
+
+                    if (compat != null)
+                    {
+                        compat.SystemBarsBehavior = WindowInsetsControllerCompat.BehaviorShowTransientBarsBySwipe;
+                    }
+                }
+            }
+        }
+
+        internal void ApplyStatusBarState()
+        {
+            IsSystemBarVisible = _systemUiVisibility;
+            SystemBarTheme = _statusBarTheme;
+        }
+
+        private class InsetsAnimationCallback : WindowInsetsAnimationCompat.Callback
+        {
+            public InsetsAnimationCallback(int dispatchMode) : base(dispatchMode)
+            {
+            }
+
+            public AndroidInsetsManager InsetsManager { get; set; }
+
+            public override WindowInsetsCompat OnProgress(WindowInsetsCompat insets, IList<WindowInsetsAnimationCompat> runningAnimations)
+            {
+                foreach (var anim in runningAnimations)
+                {
+                    if ((anim.TypeMask & WindowInsetsCompat.Type.Ime()) != 0)
+                    {
+                        var renderScaling = InsetsManager._topLevel.RenderScaling;
+
+                        var inset = insets.GetInsets((InsetsManager.DisplayEdgeToEdge ? WindowInsetsCompat.Type.StatusBars() | WindowInsetsCompat.Type.NavigationBars() | WindowInsetsCompat.Type.DisplayCutout() : 0) | WindowInsetsCompat.Type.Ime());
+                        var navBarInset = insets.GetInsets(WindowInsetsCompat.Type.NavigationBars());
+                        var imeInset = insets.GetInsets(WindowInsetsCompat.Type.Ime());
+
+
+                        var bottomPadding = (imeInset.Bottom > 0 && !InsetsManager.DisplayEdgeToEdge ? imeInset.Bottom - navBarInset.Bottom : inset.Bottom);
+                        bottomPadding = (int)(bottomPadding * anim.InterpolatedFraction);
+
+                        var padding = new Thickness(inset.Left / renderScaling,
+                            inset.Top / renderScaling,
+                            inset.Right / renderScaling,
+                            bottomPadding / renderScaling);
+                        InsetsManager?.NotifySafeAreaChanged(padding);
+                        break;
+                    }
+                }
+                return insets;
+            }
+        }
+    }
+}

+ 24 - 19
src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs

@@ -3,9 +3,7 @@ using System.Collections.Generic;
 using Android.App;
 using Android.Content;
 using Android.Graphics;
-using Android.OS;
 using Android.Runtime;
-using Android.Text;
 using Android.Views;
 using Android.Views.InputMethods;
 using Avalonia.Android.Platform.Specific;
@@ -24,11 +22,13 @@ using Avalonia.Platform.Storage;
 using Avalonia.Rendering;
 using Avalonia.Rendering.Composition;
 using Java.Lang;
+using Java.Util;
 using Math = System.Math;
 using AndroidRect = Android.Graphics.Rect;
 using Window = Android.Views.Window;
 using Android.Graphics.Drawables;
-using Java.Util;
+using Android.OS;
+using Android.Text;
 
 namespace Avalonia.Android.Platform.SkiaPlatform
 {
@@ -43,6 +43,7 @@ namespace Avalonia.Android.Platform.SkiaPlatform
         private readonly INativeControlHostImpl _nativeControlHost;
         private readonly IStorageProvider _storageProvider;
         private readonly ISystemNavigationManagerImpl _systemNavigationManager;
+        private readonly AndroidInsetsManager _insetsManager;
         private ViewImpl _view;
 
         public TopLevelImpl(AvaloniaView avaloniaView, bool placeOnTop = false)
@@ -59,6 +60,11 @@ namespace Avalonia.Android.Platform.SkiaPlatform
             MaxClientSize = new PixelSize(_view.Resources.DisplayMetrics.WidthPixels,
                 _view.Resources.DisplayMetrics.HeightPixels).ToSize(RenderScaling);
 
+            if (avaloniaView.Context is AvaloniaMainActivity mainActivity)
+            {
+                _insetsManager = new AndroidInsetsManager(mainActivity, this);
+            }
+
             _nativeControlHost = new AndroidNativeControlHostImpl(avaloniaView);
             _storageProvider = new AndroidStorageProvider((Activity)avaloniaView.Context);
 
@@ -70,21 +76,7 @@ namespace Avalonia.Android.Platform.SkiaPlatform
 
         public IInputRoot InputRoot { get; private set; }
 
-        public virtual Size ClientSize
-        {
-            get
-            {
-                AndroidRect rect = new AndroidRect();
-                AndroidRect intersection = new AndroidRect();
-
-                _view.GetWindowVisibleDisplayFrame(intersection);
-                _view.GetGlobalVisibleRect(rect);
-
-                intersection.Intersect(rect);
-
-                return new PixelSize(intersection.Right - intersection.Left, intersection.Bottom - intersection.Top).ToSize(RenderScaling);
-            }
-        }
+        public virtual Size ClientSize => _view.Size.ToSize(RenderScaling);
 
         public Size? FrameSize => null;
 
@@ -285,7 +277,15 @@ namespace Avalonia.Android.Platform.SkiaPlatform
 
         public void SetFrameThemeVariant(PlatformThemeVariant themeVariant)
         {
-            // TODO adjust status bar depending on full screen mode.
+            if(_insetsManager != null)
+            {
+                _insetsManager.SystemBarTheme = themeVariant switch
+                {
+                    PlatformThemeVariant.Light => SystemBarTheme.Light,
+                    PlatformThemeVariant.Dark => SystemBarTheme.Dark,
+                    _ => null,
+                };
+            }
         }
 
         public AcrylicPlatformCompensationLevels AcrylicCompensationLevels => new AcrylicPlatformCompensationLevels(1, 1, 1);
@@ -403,6 +403,11 @@ namespace Avalonia.Android.Platform.SkiaPlatform
                 return _nativeControlHost;
             }
 
+            if (featureType == typeof(IInsetsManager))
+            {
+                return _insetsManager;
+            }
+
             return null;
         }
     }

+ 33 - 17
src/Android/Avalonia.Android/Platform/Storage/AndroidStorageItem.cs

@@ -138,27 +138,43 @@ internal class AndroidStorageFolder : AndroidStorageItem, IStorageBookmarkFolder
             return Array.Empty<IStorageItem>();
         }
 
-        using var javaFile = new JavaFile(Uri.Path!);
+        List<IStorageItem> files = new List<IStorageItem>();
 
-        // Java file represents files AND directories. Don't be confused.
-        var files = await javaFile.ListFilesAsync().ConfigureAwait(false);
-        if (files is null)
+        var contentResolver = Activity.ContentResolver;
+        if (contentResolver == null)
         {
-            return Array.Empty<IStorageItem>();
+            return files;
         }
 
-        return files
-            .Select(f => (file: f, uri: AndroidUri.FromFile(f)))
-            .Where(t => t.uri is not null)
-            .Select(t => t.file switch
-            {
-                { IsFile: true } => (IStorageItem)new AndroidStorageFile(Activity, t.uri!),
-                { IsDirectory: true } => new AndroidStorageFolder(Activity, t.uri!, false),
-                _ => null
-            })
-            .Where(i => i is not null)
-            .ToArray()!;
-    }
+        var childrenUri = DocumentsContract.BuildChildDocumentsUriUsingTree(Uri!, DocumentsContract.GetTreeDocumentId(Uri));
+
+        var projection = new[]
+        {
+            DocumentsContract.Document.ColumnDocumentId,
+            DocumentsContract.Document.ColumnMimeType
+        };
+        if (childrenUri != null)
+        {
+            using var cursor = contentResolver.Query(childrenUri, projection, null, null, null);
+
+            if (cursor != null)
+                while (cursor.MoveToNext())
+                {
+                    var mime = cursor.GetString(1);
+                    var id = cursor.GetString(0);
+                    var uri = DocumentsContract.BuildDocumentUriUsingTree(Uri!, id);
+                    if (uri == null)
+                    {
+                        continue;
+                    }
+
+                    files.Add(mime == DocumentsContract.Document.MimeTypeDir ? new AndroidStorageFolder(Activity, uri, false) :
+                        new AndroidStorageFile(Activity, uri));
+                }
+        }
+
+        return files;
+    }       
 }
 
 internal sealed class WellKnownAndroidStorageFolder : AndroidStorageFolder

+ 6 - 1
src/Avalonia.Base/AttachedProperty.cs

@@ -32,9 +32,14 @@ namespace Avalonia
         /// </summary>
         /// <typeparam name="TOwner">The owner type.</typeparam>
         /// <returns>The property.</returns>
-        public new AttachedProperty<TValue> AddOwner<TOwner>() where TOwner : AvaloniaObject
+        public new AttachedProperty<TValue> AddOwner<TOwner>(StyledPropertyMetadata<TValue>? metadata = null) where TOwner : AvaloniaObject
         {
             AvaloniaPropertyRegistry.Instance.Register(typeof(TOwner), this);
+            if (metadata != null)
+            {
+                OverrideMetadata<TOwner>(metadata);
+            }
+            
             return this;
         }
     }

+ 39 - 39
src/Avalonia.Base/AvaloniaProperty.cs

@@ -4,7 +4,6 @@ using System.Diagnostics.CodeAnalysis;
 using Avalonia.Data;
 using Avalonia.Data.Core;
 using Avalonia.PropertyStore;
-using Avalonia.Styling;
 using Avalonia.Utilities;
 
 namespace Avalonia
@@ -20,12 +19,20 @@ namespace Avalonia
         public static readonly object UnsetValue = new UnsetValueType();
 
         private static int s_nextId;
+
+        /// <summary>
+        /// Provides a metadata object for types which have no metadata of their own.
+        /// </summary>
         private readonly AvaloniaPropertyMetadata _defaultMetadata;
+
+        /// <summary>
+        /// Provides a fast path when the property has no metadata overrides.
+        /// </summary>
+        private KeyValuePair<Type, AvaloniaPropertyMetadata>? _singleMetadata;
+
         private readonly Dictionary<Type, AvaloniaPropertyMetadata> _metadata;
         private readonly Dictionary<Type, AvaloniaPropertyMetadata> _metadataCache = new Dictionary<Type, AvaloniaPropertyMetadata>();
 
-        private bool _hasMetadataOverrides;
-
         /// <summary>
         /// Initializes a new instance of the <see cref="AvaloniaProperty"/> class.
         /// </summary>
@@ -57,7 +64,8 @@ namespace Avalonia
             Id = s_nextId++;
 
             _metadata.Add(ownerType, metadata ?? throw new ArgumentNullException(nameof(metadata)));
-            _defaultMetadata = metadata;
+            _defaultMetadata = metadata.GenerateTypeSafeMetadata();
+            _singleMetadata = new(ownerType, metadata);
         }
 
         /// <summary>
@@ -80,9 +88,6 @@ namespace Avalonia
             Id = source.Id;
             _defaultMetadata = source._defaultMetadata;
 
-            // Properties that have different owner can't use fast path for metadata.
-            _hasMetadataOverrides = true;
-
             if (metadata != null)
             {
                 _metadata.Add(ownerType, metadata);
@@ -257,7 +262,18 @@ namespace Avalonia
             return result;
         }
 
-        /// <inheritdoc cref="Register{TOwner, TValue}" />
+        /// <summary>
+        /// Registers an attached <see cref="AvaloniaProperty"/>.
+        /// </summary>
+        /// <typeparam name="TOwner">The type of the class that is registering the property.</typeparam>
+        /// <typeparam name="TValue">The type of the property's value.</typeparam>
+        /// <param name="name">The name of the property.</param>
+        /// <param name="defaultValue">The default value of the property.</param>
+        /// <param name="inherits">Whether the property inherits its value.</param>
+        /// <param name="defaultBindingMode">The default binding mode for the property.</param>
+        /// <param name="validate">A value validation callback.</param>
+        /// <param name="coerce">A value coercion callback.</param>
+        /// <param name="enableDataValidation">if is set to true enable data validation.</param>
         /// <param name="notifying">
         /// A method that gets called before and after the property starts being notified on an
         /// object; the bool argument will be true before and false afterwards. This callback is
@@ -442,33 +458,14 @@ namespace Avalonia
         }
 
         /// <summary>
-        /// Gets the property metadata for the specified type.
+        /// Gets the <see cref="AvaloniaPropertyMetadata"/> which applies to this property when it is used with the specified type.
         /// </summary>
-        /// <typeparam name="T">The type.</typeparam>
-        /// <returns>
-        /// The property metadata.
-        /// </returns>
-        public AvaloniaPropertyMetadata GetMetadata<T>() where T : AvaloniaObject
-        {
-            return GetMetadata(typeof(T));
-        }
+        /// <typeparam name="T">The type for which to retrieve metadata.</typeparam>
+        public AvaloniaPropertyMetadata GetMetadata<T>() where T : AvaloniaObject => GetMetadata(typeof(T));
 
-        /// <summary>
-        /// Gets the property metadata for the specified type.
-        /// </summary>
-        /// <param name="type">The type.</param>
-        /// <returns>
-        /// The property metadata.
-        /// </returns>
-        public AvaloniaPropertyMetadata GetMetadata(Type type)
-        {
-            if (!_hasMetadataOverrides)
-            {
-                return _defaultMetadata;
-            }
-
-            return GetMetadataWithOverrides(type);
-        }
+        /// <inheritdoc cref="GetMetadata{T}"/>
+        /// <param name="type">The type for which to retrieve metadata.</param>
+        public AvaloniaPropertyMetadata GetMetadata(Type type) => GetMetadataWithOverrides(type);
 
         /// <summary>
         /// Checks whether the <paramref name="value"/> is valid for the property.
@@ -567,7 +564,7 @@ namespace Avalonia
             _metadata.Add(type, metadata);
             _metadataCache.Clear();
 
-            _hasMetadataOverrides = true;
+            _singleMetadata = null;
         }
 
         protected abstract IObservable<AvaloniaPropertyChangedEventArgs> GetChanged();
@@ -584,7 +581,12 @@ namespace Avalonia
                 return result;
             }
 
-            Type? currentType = type;
+            if (_singleMetadata is { } singleMetadata)
+            {
+                return _metadataCache[type] = singleMetadata.Key.IsAssignableFrom(type) ? singleMetadata.Value : _defaultMetadata;
+            }
+
+            var currentType = type;
 
             while (currentType != null)
             {
@@ -598,13 +600,11 @@ namespace Avalonia
                 currentType = currentType.BaseType;
             }
 
-            _metadataCache[type] = _defaultMetadata;
-
-            return _defaultMetadata;
+            return _metadataCache[type] = _defaultMetadata;
         }
 
         bool IPropertyInfo.CanGet => true;
-        bool IPropertyInfo.CanSet => true;
+        bool IPropertyInfo.CanSet => !IsReadOnly;
         object? IPropertyInfo.Get(object target) => ((AvaloniaObject)target).GetValue(this);
         void IPropertyInfo.Set(object target, object? value) => ((AvaloniaObject)target).SetValue(this, value);
     }

+ 9 - 1
src/Avalonia.Base/AvaloniaPropertyMetadata.cs

@@ -5,7 +5,7 @@ namespace Avalonia
     /// <summary>
     /// Base class for avalonia property metadata.
     /// </summary>
-    public class AvaloniaPropertyMetadata
+    public abstract class AvaloniaPropertyMetadata
     {
         private BindingMode _defaultBindingMode;
 
@@ -61,5 +61,13 @@ namespace Avalonia
 
             EnableDataValidation ??= baseMetadata.EnableDataValidation;
         }
+
+        /// <summary>
+        /// Gets a copy of this object configured for use with any owner type.
+        /// </summary>
+        /// <remarks>
+        /// For example, delegates which receive the owner object should be removed.
+        /// </remarks>
+        public abstract AvaloniaPropertyMetadata GenerateTypeSafeMetadata();
     }
 }

+ 1 - 1
src/Avalonia.Base/AvaloniaPropertyRegistry.cs

@@ -364,7 +364,7 @@ namespace Avalonia
         /// <param name="property">The property.</param>
         /// <remarks>
         /// You won't usually want to call this method directly, instead use the
-        /// <see cref="AvaloniaProperty.Register{TOwner, TValue}(string, TValue, bool, Data.BindingMode, Func{TValue, bool}, Func{AvaloniaObject, TValue, TValue}, Action{AvaloniaObject, bool})"/>
+        /// <see cref="AvaloniaProperty.Register{TOwner, TValue}(string, TValue, bool, Data.BindingMode, Func{TValue, bool}, Func{AvaloniaObject, TValue, TValue}, bool)"/>
         /// method.
         /// </remarks>
         public void Register(Type type, AvaloniaProperty property)

+ 2 - 0
src/Avalonia.Base/DirectPropertyMetadata`1.cs

@@ -45,5 +45,7 @@ namespace Avalonia
                 UnsetValue ??= src.UnsetValue;
             }
         }
+
+        public override AvaloniaPropertyMetadata GenerateTypeSafeMetadata() => new DirectPropertyMetadata<TValue>(UnsetValue, DefaultBindingMode, EnableDataValidation);
     }
 }

+ 7 - 8
src/Avalonia.Base/Input/GestureRecognizers/GestureRecognizerCollection.cs

@@ -2,7 +2,7 @@ using System.Collections;
 using System.Collections.Generic;
 using Avalonia.Controls;
 using Avalonia.LogicalTree;
-using Avalonia.Styling;
+using Avalonia.Reactive;
 
 namespace Avalonia.Input.GestureRecognizers
 {
@@ -11,13 +11,13 @@ namespace Avalonia.Input.GestureRecognizers
         private readonly IInputElement _inputElement;
         private List<IGestureRecognizer>? _recognizers;
         private Dictionary<IPointer, IGestureRecognizer>? _pointerGrabs;
-        
-        
+
+
         public GestureRecognizerCollection(IInputElement inputElement)
         {
             _inputElement = inputElement;
         }
-        
+
         public void Add(IGestureRecognizer recognizer)
         {
             if (_recognizers == null)
@@ -31,14 +31,13 @@ namespace Avalonia.Input.GestureRecognizers
             recognizer.Initialize(_inputElement, this);
 
             // Hacks to make bindings work
-            
+
             if (_inputElement is ILogical logicalParent && recognizer is ISetLogicalParent logical)
             {
                 logical.SetParent(logicalParent);
                 if (recognizer is StyledElement styleableRecognizer
                     && _inputElement is StyledElement styleableParent)
-                    styleableRecognizer.Bind(StyledElement.TemplatedParentProperty,
-                        styleableParent.GetObservable(StyledElement.TemplatedParentProperty));
+                    styleableParent.GetObservable(StyledElement.TemplatedParentProperty).Subscribe(parent => styleableRecognizer.TemplatedParent = parent);
             }
         }
 
@@ -58,7 +57,7 @@ namespace Avalonia.Input.GestureRecognizers
                 return false;
             foreach (var r in _recognizers)
             {
-                if(e.Handled)
+                if (e.Handled)
                     break;
                 r.PointerPressed(e);
             }

+ 4 - 8
src/Avalonia.Base/Input/GestureRecognizers/PullGestureRecognizer.cs

@@ -13,22 +13,18 @@ namespace Avalonia.Input
         private Point _initialPosition;
         private int _gestureId;
         private IPointer? _tracking;
-        private PullDirection _pullDirection;
         private bool _pullInProgress;
 
         /// <summary>
         /// Defines the <see cref="PullDirection"/> property.
         /// </summary>
-        public static readonly DirectProperty<PullGestureRecognizer, PullDirection> PullDirectionProperty =
-            AvaloniaProperty.RegisterDirect<PullGestureRecognizer, PullDirection>(
-                nameof(PullDirection),
-                o => o.PullDirection,
-                (o, v) => o.PullDirection = v);
+        public static readonly StyledProperty<PullDirection> PullDirectionProperty =
+            AvaloniaProperty.Register<PullGestureRecognizer, PullDirection>(nameof(PullDirection));
 
         public PullDirection PullDirection
         {
-            get => _pullDirection;
-            set => SetAndRaise(PullDirectionProperty, ref _pullDirection, value);
+            get => GetValue(PullDirectionProperty);
+            set => SetValue(PullDirectionProperty, value);
         }
 
         public PullGestureRecognizer(PullDirection pullDirection)

+ 18 - 34
src/Avalonia.Base/Input/GestureRecognizers/ScrollGestureRecognizer.cs

@@ -17,61 +17,45 @@ namespace Avalonia.Input.GestureRecognizers
         private IPointer? _tracking;
         private IInputElement? _target;
         private IGestureRecognizerActionsDispatcher? _actions;
-        private bool _canHorizontallyScroll;
-        private bool _canVerticallyScroll;
         private int _gestureId;
-        private int _scrollStartDistance = 30;
         private Point _pointerPressedPoint;
         private VelocityTracker? _velocityTracker;
 
         // Movement per second
         private Vector _inertia;
         private ulong? _lastMoveTimestamp;
-        private bool _isScrollInertiaEnabled;
 
         /// <summary>
         /// Defines the <see cref="CanHorizontallyScroll"/> property.
         /// </summary>
-        public static readonly DirectProperty<ScrollGestureRecognizer, bool> CanHorizontallyScrollProperty =
-            AvaloniaProperty.RegisterDirect<ScrollGestureRecognizer, bool>(
-                nameof(CanHorizontallyScroll),
-                o => o.CanHorizontallyScroll,
-                (o, v) => o.CanHorizontallyScroll = v);
+        public static readonly StyledProperty<bool> CanHorizontallyScrollProperty =
+            AvaloniaProperty.Register<ScrollGestureRecognizer, bool>(nameof(CanHorizontallyScroll));
 
         /// <summary>
         /// Defines the <see cref="CanVerticallyScroll"/> property.
         /// </summary>
-        public static readonly DirectProperty<ScrollGestureRecognizer, bool> CanVerticallyScrollProperty =
-            AvaloniaProperty.RegisterDirect<ScrollGestureRecognizer, bool>(
-                nameof(CanVerticallyScroll),
-                o => o.CanVerticallyScroll,
-                (o, v) => o.CanVerticallyScroll = v);
+        public static readonly StyledProperty<bool> CanVerticallyScrollProperty =
+            AvaloniaProperty.Register<ScrollGestureRecognizer, bool>(nameof(CanVerticallyScroll));
 
         /// <summary>
         /// Defines the <see cref="IsScrollInertiaEnabled"/> property.
         /// </summary>
-        public static readonly DirectProperty<ScrollGestureRecognizer, bool> IsScrollInertiaEnabledProperty =
-            AvaloniaProperty.RegisterDirect<ScrollGestureRecognizer, bool>(
-                nameof(IsScrollInertiaEnabled),
-                o => o.IsScrollInertiaEnabled,
-                (o, v) => o.IsScrollInertiaEnabled = v);
+        public static readonly StyledProperty<bool> IsScrollInertiaEnabledProperty =
+            AvaloniaProperty.Register<ScrollGestureRecognizer, bool>(nameof(IsScrollInertiaEnabled));
 
         /// <summary>
         /// Defines the <see cref="ScrollStartDistance"/> property.
         /// </summary>
-        public static readonly DirectProperty<ScrollGestureRecognizer, int> ScrollStartDistanceProperty =
-            AvaloniaProperty.RegisterDirect<ScrollGestureRecognizer, int>(
-                nameof(ScrollStartDistance),
-                o => o.ScrollStartDistance,
-                (o, v) => o.ScrollStartDistance = v);
+        public static readonly StyledProperty<int> ScrollStartDistanceProperty =
+            AvaloniaProperty.Register<ScrollGestureRecognizer, int>(nameof(ScrollStartDistance), 30);
         
         /// <summary>
         /// Gets or sets a value indicating whether the content can be scrolled horizontally.
         /// </summary>
         public bool CanHorizontallyScroll
         {
-            get => _canHorizontallyScroll;
-            set => SetAndRaise(CanHorizontallyScrollProperty, ref _canHorizontallyScroll, value);
+            get => GetValue(CanHorizontallyScrollProperty);
+            set => SetValue(CanHorizontallyScrollProperty, value);
         }
 
         /// <summary>
@@ -79,8 +63,8 @@ namespace Avalonia.Input.GestureRecognizers
         /// </summary>
         public bool CanVerticallyScroll
         {
-            get => _canVerticallyScroll;
-            set => SetAndRaise(CanVerticallyScrollProperty, ref _canVerticallyScroll, value);
+            get => GetValue(CanVerticallyScrollProperty);
+            set => SetValue(CanVerticallyScrollProperty, value);
         }
         
         /// <summary>
@@ -88,8 +72,8 @@ namespace Avalonia.Input.GestureRecognizers
         /// </summary>
         public bool IsScrollInertiaEnabled
         {
-            get => _isScrollInertiaEnabled;
-            set => SetAndRaise(IsScrollInertiaEnabledProperty, ref _isScrollInertiaEnabled, value);
+            get => GetValue(IsScrollInertiaEnabledProperty);
+            set => SetValue(IsScrollInertiaEnabledProperty, value);
         }
 
         /// <summary>
@@ -97,8 +81,8 @@ namespace Avalonia.Input.GestureRecognizers
         /// </summary>
         public int ScrollStartDistance
         {
-            get => _scrollStartDistance;
-            set => SetAndRaise(ScrollStartDistanceProperty, ref _scrollStartDistance, value);
+            get => GetValue(ScrollStartDistanceProperty);
+            set => SetValue(ScrollStartDistanceProperty, value);
         }
         
 
@@ -137,8 +121,8 @@ namespace Avalonia.Input.GestureRecognizers
                         
                         // Correct _trackedRootPoint with ScrollStartDistance, so scrolling does not start with a skip of ScrollStartDistance
                         _trackedRootPoint = new Point(
-                            _trackedRootPoint.X - (_trackedRootPoint.X >= rootPoint.X ? _scrollStartDistance : -_scrollStartDistance),
-                            _trackedRootPoint.Y - (_trackedRootPoint.Y >= rootPoint.Y ? _scrollStartDistance : -_scrollStartDistance));
+                            _trackedRootPoint.X - (_trackedRootPoint.X >= rootPoint.X ? ScrollStartDistance : -ScrollStartDistance),
+                            _trackedRootPoint.Y - (_trackedRootPoint.Y >= rootPoint.Y ? ScrollStartDistance : -ScrollStartDistance));
 
                         _actions!.Capture(e.Pointer, this);
                     }

+ 1 - 0
src/Avalonia.Base/Input/MouseDevice.cs

@@ -4,6 +4,7 @@ using Avalonia.Reactive;
 using Avalonia.Input.Raw;
 using Avalonia.Platform;
 using Avalonia.Utilities;
+#pragma warning disable CS0618
 
 namespace Avalonia.Input
 {

+ 2 - 1
src/Avalonia.Base/Input/PenDevice.cs

@@ -4,6 +4,7 @@ using System.Linq;
 using System.Reflection;
 using Avalonia.Input.Raw;
 using Avalonia.Platform;
+#pragma warning disable CS0618
 
 namespace Avalonia.Input
 {
@@ -129,7 +130,7 @@ namespace Avalonia.Input
                 var e = new PointerReleasedEventArgs(source, pointer, (Visual)root, p, timestamp, properties, inputModifiers,
                     _lastMouseDownButton);
 
-                source?.RaiseEvent(e);
+                source.RaiseEvent(e);
                 pointer.Capture(null);
                 _lastMouseDownButton = default;
                 return e.Handled;

+ 2 - 0
src/Avalonia.Base/Input/PointerEventArgs.cs

@@ -42,7 +42,9 @@ namespace Avalonia.Input
             PointerPointProperties properties,
             KeyModifiers modifiers,
             Lazy<IReadOnlyList<RawPointerPoint>?>? previousPoints)
+#pragma warning disable CS0618
             : this(routedEvent, source, pointer, rootVisual, rootVisualPosition, timestamp, properties, modifiers)
+#pragma warning restore CS0618
         {
             _previousPoints = previousPoints;
         }

+ 20 - 13
src/Avalonia.Base/Input/PointerOverPreProcessor.cs

@@ -6,7 +6,8 @@ namespace Avalonia.Input
     internal class PointerOverPreProcessor : IObserver<RawInputEventArgs>
     {
         private IPointerDevice? _lastActivePointerDevice;
-        private (IPointer pointer, PixelPoint position)? _lastPointer;
+        private (IPointer pointer, PixelPoint position)? _currentPointer;
+        private PixelPoint? _lastKnownPosition;
 
         private readonly IInputRoot _inputRoot;
 
@@ -15,7 +16,7 @@ namespace Avalonia.Input
             _inputRoot = inputRoot ?? throw new ArgumentNullException(nameof(inputRoot));
         }
 
-        public PixelPoint? LastPosition => _lastPointer?.position;
+        public PixelPoint? LastPosition => _lastKnownPosition;
         
         public void OnCompleted()
         {
@@ -41,14 +42,14 @@ namespace Avalonia.Input
                 }
 
                 if (args.Type is RawPointerEventType.LeaveWindow or RawPointerEventType.NonClientLeftButtonDown
-                    && _lastPointer is (var lastPointer, var lastPosition))
+                    && _currentPointer is var (lastPointer, lastPosition))
                 {
-                    _lastPointer = null;
+                    _currentPointer = null;
                     ClearPointerOver(lastPointer, args.Root, 0, PointToClient(args.Root, lastPosition),
                         new PointerPointProperties(args.InputModifiers, args.Type.ToUpdateKind()),
                         args.InputModifiers.ToKeyModifiers());
                 }
-                else if (pointerDevice.TryGetPointer(args) is IPointer pointer
+                else if (pointerDevice.TryGetPointer(args) is { } pointer
                     && pointer.Type != PointerType.Touch)
                 {
                     var element = pointer.Captured ?? args.InputHitTestResult;
@@ -62,7 +63,7 @@ namespace Avalonia.Input
 
         public void SceneInvalidated(Rect dirtyRect)
         {
-            if (_lastPointer is (var pointer, var position))
+            if (_currentPointer is (var pointer, var position))
             {
                 var clientPoint = PointToClient(_inputRoot, position);
 
@@ -80,12 +81,12 @@ namespace Avalonia.Input
 
         private void ClearPointerOver()
         {
-            if (_lastPointer is (var pointer, var position))
+            if (_currentPointer is (var pointer, var position))
             {
                 var clientPoint = PointToClient(_inputRoot, position);
                 ClearPointerOver(pointer, _inputRoot, 0, clientPoint, PointerPointProperties.None, KeyModifiers.None);
             }
-            _lastPointer = null;
+            _currentPointer = null;
             _lastActivePointerDevice = null;
         }
 
@@ -100,9 +101,11 @@ namespace Avalonia.Input
 
             // Do not pass rootVisual, when we have unknown position,
             // so GetPosition won't return invalid values.
+#pragma warning disable CS0618
             var e = new PointerEventArgs(InputElement.PointerExitedEvent, element, pointer,
                 position.HasValue ? root as Visual : null, position.HasValue ? position.Value : default,
                 timestamp, properties, inputModifiers);
+#pragma warning restore CS0618
 
             if (element is Visual v && !v.IsAttachedToVisualTree)
             {
@@ -122,18 +125,18 @@ namespace Avalonia.Input
 
             root.PointerOverElement = null;
             _lastActivePointerDevice = null;
-            _lastPointer = null;
+            _currentPointer = null;
         }
 
         private void ClearChildrenPointerOver(PointerEventArgs e, IInputElement element, bool clearRoot)
         {
             if (element is Visual v)
             {
-                foreach (IInputElement el in v.VisualChildren)
+                foreach (var el in v.VisualChildren)
                 {
-                    if (el.IsPointerOver)
+                    if (el is IInputElement { IsPointerOver: true } child)
                     {
-                        ClearChildrenPointerOver(e, el, true);
+                        ClearChildrenPointerOver(e, child, true);
                         break;
                     }
                 }
@@ -151,6 +154,8 @@ namespace Avalonia.Input
             ulong timestamp, Point position, PointerPointProperties properties, KeyModifiers inputModifiers)
         {
             var pointerOverElement = root.PointerOverElement;
+            var screenPosition = ((Visual)root).PointToScreen(position);
+            _lastKnownPosition = screenPosition;
 
             if (element != pointerOverElement)
             {
@@ -164,7 +169,7 @@ namespace Avalonia.Input
                 }
             }
 
-            _lastPointer = (pointer, ((Visual)root).PointToScreen(position));
+            _currentPointer = (pointer, screenPosition);
         }
 
         private void SetPointerOverToElement(IPointer pointer, IInputRoot root, IInputElement element,
@@ -186,8 +191,10 @@ namespace Avalonia.Input
 
             el = root.PointerOverElement;
 
+#pragma warning disable CS0618
             var e = new PointerEventArgs(InputElement.PointerExitedEvent, el, pointer, (Visual)root, position,
                 timestamp, properties, inputModifiers);
+#pragma warning restore CS0618
             if (el is Visual v && branch != null && !v.IsAttachedToVisualTree)
             {
                 ClearChildrenPointerOver(e, branch, false);

+ 1 - 0
src/Avalonia.Base/Input/TouchDevice.cs

@@ -4,6 +4,7 @@ using System.Linq;
 using System.Reflection;
 using Avalonia.Input.Raw;
 using Avalonia.Platform;
+#pragma warning disable CS0618
 
 namespace Avalonia.Input
 {

+ 2 - 2
src/Avalonia.Base/Media/DrawingGroup.cs

@@ -117,10 +117,10 @@ namespace Avalonia.Media
             // root DrawingGroup, and be the same value as the root _currentDrawingGroup.
             //
             // Either way, _rootDrawing always references the root drawing.
-            protected Drawing? _rootDrawing;
+            private Drawing? _rootDrawing;
 
             // Current DrawingGroup that new children are added to
-            protected DrawingGroup? _currentDrawingGroup;
+            private DrawingGroup? _currentDrawingGroup;
 
             // Previous values of _currentDrawingGroup
             private Stack<DrawingGroup?>? _previousDrawingGroupStack;

+ 99 - 31
src/Avalonia.Base/Media/FontManager.cs

@@ -1,9 +1,11 @@
 using System;
 using System.Collections.Concurrent;
 using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
 using System.Globalization;
 using Avalonia.Media.Fonts;
 using Avalonia.Platform;
+using Avalonia.Utilities;
 
 namespace Avalonia.Media
 {
@@ -13,9 +15,11 @@ namespace Avalonia.Media
     /// </summary>
     public sealed class FontManager
     {
-        private readonly ConcurrentDictionary<Typeface, IGlyphTypeface> _glyphTypefaceCache =
-            new ConcurrentDictionary<Typeface, IGlyphTypeface>();
-        private readonly FontFamily _defaultFontFamily;
+        internal static Uri SystemFontsKey = new Uri("fonts:SystemFonts");
+
+        public const string FontCollectionScheme = "fonts";
+
+        private readonly ConcurrentDictionary<Uri, IFontCollection> _fontCollections = new ConcurrentDictionary<Uri, IFontCollection>();
         private readonly IReadOnlyList<FontFallback>? _fontFallbacks;
 
         public FontManager(IFontManagerImpl platformImpl)
@@ -33,9 +37,12 @@ namespace Avalonia.Media
                 throw new InvalidOperationException("Default font family name can't be null or empty.");
             }
 
-            _defaultFontFamily = new FontFamily(DefaultFontFamilyName);
+            AddFontCollection(new SystemFontCollection(this));
         }
 
+        /// <summary>
+        /// Get the current font manager instance.
+        /// </summary>
         public static FontManager Current
         {
             get
@@ -57,11 +64,6 @@ namespace Avalonia.Media
             }
         }
 
-        /// <summary>
-        /// 
-        /// </summary>
-        public IFontManagerImpl PlatformImpl { get; }
-
         /// <summary>
         ///     Gets the system's default font family's name.
         /// </summary>
@@ -71,41 +73,109 @@ namespace Avalonia.Media
         }
 
         /// <summary>
-        ///     Get all installed font family names.
+        ///     Get all system fonts.
         /// </summary>
-        /// <param name="checkForUpdates">If <c>true</c> the font collection is updated.</param>
-        public IEnumerable<string> GetInstalledFontFamilyNames(bool checkForUpdates = false) =>
-            PlatformImpl.GetInstalledFontFamilyNames(checkForUpdates);
+        public IFontCollection SystemFonts => _fontCollections[SystemFontsKey];
+
+        internal IFontManagerImpl PlatformImpl { get; }
 
         /// <summary>
-        ///     Returns a new <see cref="IGlyphTypeface"/>, or an existing one if a matching <see cref="IGlyphTypeface"/> exists.
+        ///     Tries to get a glyph typeface for specified typeface.
         /// </summary>
         /// <param name="typeface">The typeface.</param>
+        /// <param name="glyphTypeface">The created glyphTypeface</param>
         /// <returns>
-        ///     The <see cref="IGlyphTypeface"/>.
+        ///     <c>True</c>, if the <see cref="FontManager"/> could create the glyph typeface, <c>False</c> otherwise.
         /// </returns>
-        public IGlyphTypeface GetOrAddGlyphTypeface(Typeface typeface)
+        public bool TryGetGlyphTypeface(Typeface typeface, [NotNullWhen(true)] out IGlyphTypeface? glyphTypeface)
         {
-            while (true)
+            glyphTypeface = null;
+
+            var fontFamily = typeface.FontFamily;
+
+            if (fontFamily.Key is FontFamilyKey key)
             {
-                if (_glyphTypefaceCache.TryGetValue(typeface, out var glyphTypeface))
+                var source = key.Source;
+
+                if (!source.IsAbsoluteUri)
                 {
-                    return glyphTypeface;
+                    if (key.BaseUri == null)
+                    {
+                        throw new NotSupportedException($"{nameof(key.BaseUri)} can't be null.");
+                    }
+
+                    source = new Uri(key.BaseUri, source);
                 }
 
-                glyphTypeface = PlatformImpl.CreateGlyphTypeface(typeface);
+                if (!_fontCollections.TryGetValue(source, out var fontCollection) && (source.IsAbsoluteResm() || source.IsAvares()))
+                {
+                    var embeddedFonts = new EmbeddedFontCollection(source, source);
+
+                    embeddedFonts.Initialize(PlatformImpl);
 
-                if (_glyphTypefaceCache.TryAdd(typeface, glyphTypeface))
+                    if (embeddedFonts.Count > 0 && _fontCollections.TryAdd(source, embeddedFonts))
+                    {
+                        fontCollection = embeddedFonts;
+                    }
+                }
+
+                if (fontCollection != null && fontCollection.TryGetGlyphTypeface(fontFamily.FamilyNames.PrimaryFamilyName,
+                    typeface.Style, typeface.Weight, typeface.Stretch, out glyphTypeface))
                 {
-                    return glyphTypeface;
+                    return true;
                 }
 
-                if (typeface.FontFamily == _defaultFontFamily)
+                if (!fontFamily.FamilyNames.HasFallbacks)
                 {
-                   throw new InvalidOperationException($"Could not create glyph typeface for: {typeface.FontFamily.Name}.");
+                    return false;
                 }
+            }
 
-                typeface = new Typeface(_defaultFontFamily, typeface.Style, typeface.Weight);
+            foreach (var familyName in fontFamily.FamilyNames)
+            {
+                if (SystemFonts.TryGetGlyphTypeface(familyName, typeface.Style, typeface.Weight, typeface.Stretch, out glyphTypeface))
+                {
+                    return true;
+                }
+            }
+
+            return SystemFonts.TryGetGlyphTypeface(DefaultFontFamilyName, typeface.Style, typeface.Weight, typeface.Stretch, out glyphTypeface);
+        }
+
+        /// <summary>
+        /// Add a font collection to the manager.
+        /// </summary>
+        /// <param name="fontCollection">The font collection.</param>
+        /// <exception cref="ArgumentException"></exception>
+        /// <remarks>If a font collection's key is already present the collection is replaced.</remarks>
+        public void AddFontCollection(IFontCollection fontCollection)
+        {
+            var key = fontCollection.Key;
+
+            if (!fontCollection.Key.IsFontCollection())
+            {
+                throw new ArgumentException("Font collection Key should follow the fonts: scheme.", nameof(fontCollection));
+            }
+
+            _fontCollections.AddOrUpdate(key, fontCollection, (_, oldCollection) =>
+            {
+                oldCollection.Dispose();
+
+                return fontCollection;
+            });
+
+            fontCollection.Initialize(PlatformImpl);
+        }
+
+        /// <summary>
+        /// Removes the font collection that corresponds to specified key.
+        /// </summary>
+        /// <param name="key">The font collection's key.</param>
+        public void RemoveFontCollection(Uri key)
+        {
+            if (_fontCollections.TryRemove(key, out var fontCollection))
+            {
+                fontCollection.Dispose();
             }
         }
 
@@ -123,18 +193,16 @@ namespace Avalonia.Media
         ///     <c>True</c>, if the <see cref="FontManager"/> could match the character to specified parameters, <c>False</c> otherwise.
         /// </returns>
         public bool TryMatchCharacter(int codepoint, FontStyle fontStyle, FontWeight fontWeight,
-            FontStretch fontStretch,
-            FontFamily? fontFamily, CultureInfo? culture, out Typeface typeface)
+            FontStretch fontStretch, FontFamily? fontFamily, CultureInfo? culture, out Typeface typeface)
         {
-            if(_fontFallbacks != null)
+            if (_fontFallbacks != null)
             {
                 foreach (var fallback in _fontFallbacks)
                 {
                     typeface = new Typeface(fallback.FontFamily, fontStyle, fontWeight, fontStretch);
 
-                    var glyphTypeface = GetOrAddGlyphTypeface(typeface);
-
-                    if(glyphTypeface.TryGetGlyph((uint)codepoint, out _)){
+                    if (TryGetGlyphTypeface(typeface, out var glyphTypeface) && glyphTypeface.TryGetGlyph((uint)codepoint, out _))
+                    {
                         return true;
                     }
                 }

+ 290 - 0
src/Avalonia.Base/Media/Fonts/EmbeddedFontCollection.cs

@@ -0,0 +1,290 @@
+using System;
+using System.Collections;
+using System.Collections.Concurrent;
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using System.Globalization;
+using Avalonia.Platform;
+
+namespace Avalonia.Media.Fonts
+{
+    public class EmbeddedFontCollection : IFontCollection
+    {
+        private readonly ConcurrentDictionary<string, ConcurrentDictionary<FontCollectionKey, IGlyphTypeface>> _glyphTypefaceCache = new();
+
+        private readonly List<FontFamily> _fontFamilies = new List<FontFamily>(1);
+
+        private readonly Uri _key;
+
+        private readonly Uri _source;
+
+        public EmbeddedFontCollection(Uri key, Uri source)
+        {
+            _key = key;
+
+            _source = source;
+        }
+
+        public Uri Key => _key;
+
+        public FontFamily this[int index] => _fontFamilies[index];
+
+        public int Count => _fontFamilies.Count;
+
+        public void Initialize(IFontManagerImpl fontManager)
+        {
+            var assetLoader = AvaloniaLocator.Current.GetRequiredService<IAssetLoader>();
+
+            var fontAssets = FontFamilyLoader.LoadFontAssets(_source);
+
+            foreach (var fontAsset in fontAssets)
+            {
+                var stream = assetLoader.Open(fontAsset);
+
+                if (fontManager.TryCreateGlyphTypeface(stream, out var glyphTypeface))
+                {
+                    if (!_glyphTypefaceCache.TryGetValue(glyphTypeface.FamilyName, out var glyphTypefaces))
+                    {
+                        glyphTypefaces = new ConcurrentDictionary<FontCollectionKey, IGlyphTypeface>();
+
+                        if (_glyphTypefaceCache.TryAdd(glyphTypeface.FamilyName, glyphTypefaces))
+                        {
+                            _fontFamilies.Add(new FontFamily(_key, glyphTypeface.FamilyName));
+                        }
+                    }
+
+                    var key = new FontCollectionKey(
+                           glyphTypeface.Style,
+                           glyphTypeface.Weight,
+                           glyphTypeface.Stretch);
+
+                    glyphTypefaces.TryAdd(key, glyphTypeface);
+                }
+            }
+        }
+
+        public void Dispose()
+        {
+            foreach (var fontFamily in _fontFamilies)
+            {
+                if (_glyphTypefaceCache.TryGetValue(fontFamily.Name, out var glyphTypefaces))
+                {
+                    foreach (var glyphTypeface in glyphTypefaces.Values)
+                    {
+                        glyphTypeface.Dispose();
+                    }
+                }
+            }
+
+            GC.SuppressFinalize(this);
+        }
+
+        public IEnumerator<FontFamily> GetEnumerator() => _fontFamilies.GetEnumerator();
+
+        IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
+
+        public bool TryGetGlyphTypeface(string familyName, FontStyle style, FontWeight weight,
+            FontStretch stretch, [NotNullWhen(true)] out IGlyphTypeface? glyphTypeface)
+        {
+            var key = new FontCollectionKey(style, weight, stretch);
+
+            if (_glyphTypefaceCache.TryGetValue(familyName, out var glyphTypefaces))
+            {
+                if (TryGetNearestMatch(glyphTypefaces, key, out glyphTypeface))
+                {
+                    return true;
+                }
+            }
+
+            //Try to find a partially matching font
+            for (var i = 0; i < Count; i++)
+            {
+                var fontFamily = _fontFamilies[i];
+
+                if (fontFamily.Name.ToLower(CultureInfo.InvariantCulture).StartsWith(familyName.ToLower(CultureInfo.InvariantCulture)))
+                {
+                    if (_glyphTypefaceCache.TryGetValue(fontFamily.Name, out glyphTypefaces) &&
+                        TryGetNearestMatch(glyphTypefaces, key, out glyphTypeface))
+                    {
+                        return true;
+                    }
+                }
+            }
+
+            glyphTypeface = null;
+
+            return false;
+        }
+
+        private static bool TryGetNearestMatch(
+            ConcurrentDictionary<FontCollectionKey, IGlyphTypeface> glyphTypefaces,
+            FontCollectionKey key,
+            [NotNullWhen(true)] out IGlyphTypeface? glyphTypeface)
+        {
+            if (glyphTypefaces.TryGetValue(key, out glyphTypeface))
+            {
+                return true;
+            }
+
+            if (key.Style != FontStyle.Normal)
+            {
+                key = key with { Style = FontStyle.Normal };
+            }
+
+            if (key.Stretch != FontStretch.Normal)
+            {
+                if (TryFindStretchFallback(glyphTypefaces, key, out glyphTypeface))
+                {
+                    return true;
+                }
+
+                if (key.Weight != FontWeight.Normal)
+                {
+                    if (TryFindStretchFallback(glyphTypefaces, key with { Weight = FontWeight.Normal }, out glyphTypeface))
+                    {
+                        return true;
+                    }
+                }
+
+                key = key with { Stretch = FontStretch.Normal };
+            }
+
+            if (TryFindWeightFallback(glyphTypefaces, key, out glyphTypeface))
+            {
+                return true;
+            }
+
+            if (TryFindStretchFallback(glyphTypefaces, key, out glyphTypeface))
+            {
+                return true;
+            }
+
+            //Take the first glyph typeface we can find.
+            foreach (var typeface in glyphTypefaces.Values)
+            {
+                glyphTypeface = typeface;
+
+                return true;
+            }
+
+            return false;
+        }
+
+        private static bool TryFindStretchFallback(
+            ConcurrentDictionary<FontCollectionKey, IGlyphTypeface> glyphTypefaces,
+            FontCollectionKey key,
+            [NotNullWhen(true)] out IGlyphTypeface? glyphTypeface)
+        {
+            glyphTypeface = null;
+
+            var stretch = (int)key.Stretch;
+
+            if (stretch < 5)
+            {
+                for (var i = 0; stretch + i < 9; i++)
+                {
+                    if (glyphTypefaces.TryGetValue(key with { Stretch = (FontStretch)(stretch + i) }, out glyphTypeface))
+                    {
+                        return true;
+                    }
+                }
+            }
+            else
+            {
+                for (var i = 0; stretch - i > 1; i++)
+                {
+                    if (glyphTypefaces.TryGetValue(key with { Stretch = (FontStretch)(stretch - i) }, out glyphTypeface))
+                    {
+                        return true;
+                    }
+                }
+            }
+
+            return false;
+        }
+
+        private static bool TryFindWeightFallback(
+            ConcurrentDictionary<FontCollectionKey, IGlyphTypeface> glyphTypefaces,
+            FontCollectionKey key,
+            [NotNullWhen(true)] out IGlyphTypeface? typeface)
+        {
+            typeface = null;
+            var weight = (int)key.Weight;
+
+            //If the target weight given is between 400 and 500 inclusive          
+            if (weight >= 400 && weight <= 500)
+            {
+                //Look for available weights between the target and 500, in ascending order.
+                for (var i = 0; weight + i <= 500; i += 50)
+                {
+                    if (glyphTypefaces.TryGetValue(key with { Weight = (FontWeight)(weight + i) }, out typeface))
+                    {
+                        return true;
+                    }
+                }
+
+                //If no match is found, look for available weights less than the target, in descending order.
+                for (var i = 0; weight - i >= 100; i += 50)
+                {
+                    if (glyphTypefaces.TryGetValue(key with { Weight = (FontWeight)(weight - i) }, out typeface))
+                    {
+                        return true;
+                    }
+                }
+
+                //If no match is found, look for available weights greater than 500, in ascending order.
+                for (var i = 0; weight + i <= 900; i += 50)
+                {
+                    if (glyphTypefaces.TryGetValue(key with { Weight = (FontWeight)(weight + i) }, out typeface))
+                    {
+                        return true;
+                    }
+                }
+            }
+
+            //If a weight less than 400 is given, look for available weights less than the target, in descending order.           
+            if (weight < 400)
+            {
+                for (var i = 0; weight - i >= 100; i += 50)
+                {
+                    if (glyphTypefaces.TryGetValue(key with { Weight = (FontWeight)(weight - i) }, out typeface))
+                    {
+                        return true;
+                    }
+                }
+
+                //If no match is found, look for available weights less than the target, in descending order.
+                for (var i = 0; weight + i <= 900; i += 50)
+                {
+                    if (glyphTypefaces.TryGetValue(key with { Weight = (FontWeight)(weight + i) }, out typeface))
+                    {
+                        return true;
+                    }
+                }
+            }
+
+            //If a weight greater than 500 is given, look for available weights greater than the target, in ascending order.
+            if (weight > 500)
+            {
+                for (var i = 0; weight + i <= 900; i += 50)
+                {
+                    if (glyphTypefaces.TryGetValue(key with { Weight = (FontWeight)(weight + i) }, out typeface))
+                    {
+                        return true;
+                    }
+                }
+
+                //If no match is found, look for available weights less than the target, in descending order.
+                for (var i = 0; weight - i >= 100; i += 50)
+                {
+                    if (glyphTypefaces.TryGetValue(key with { Weight = (FontWeight)(weight - i) }, out typeface))
+                    {
+                        return true;
+                    }
+                }
+            }
+
+            return false;
+        }
+    }
+}

+ 4 - 0
src/Avalonia.Base/Media/Fonts/FontCollectionKey.cs

@@ -0,0 +1,4 @@
+namespace Avalonia.Media.Fonts
+{
+    public readonly record struct FontCollectionKey(FontStyle Style, FontWeight Weight, FontStretch Stretch);
+}

+ 32 - 34
src/Avalonia.Base/Media/Fonts/FontFamilyLoader.cs

@@ -11,22 +11,30 @@ namespace Avalonia.Media.Fonts
         /// <summary>
         /// Loads all font assets that belong to the specified <see cref="FontFamilyKey"/>
         /// </summary>
-        /// <param name="fontFamilyKey"></param>
+        /// <param name="source"></param>
         /// <returns></returns>
-        public static IEnumerable<Uri> LoadFontAssets(FontFamilyKey fontFamilyKey) =>
-            IsFontTtfOrOtf(fontFamilyKey.Source) ?
-                GetFontAssetsByExpression(fontFamilyKey) :
-                GetFontAssetsBySource(fontFamilyKey);
+        public static IEnumerable<Uri> LoadFontAssets(Uri source)
+        {
+            if (source.IsAvares() || source.IsAbsoluteResm())
+            {
+                return IsFontTtfOrOtf(source) ?
+                    GetFontAssetsByExpression(source) :
+                    GetFontAssetsBySource(source);
+            }
+
+            return Enumerable.Empty<Uri>();
+        }
+
 
         /// <summary>
         /// Searches for font assets at a given location and returns a quantity of found assets
         /// </summary>
-        /// <param name="fontFamilyKey"></param>
+        /// <param name="source"></param>
         /// <returns></returns>
-        private static IEnumerable<Uri> GetFontAssetsBySource(FontFamilyKey fontFamilyKey)
+        private static IEnumerable<Uri> GetFontAssetsBySource(Uri source)
         {
             var assetLoader = AvaloniaLocator.Current.GetRequiredService<IAssetLoader>();
-            var availableAssets = assetLoader.GetAssets(fontFamilyKey.Source, fontFamilyKey.BaseUri);
+            var availableAssets = assetLoader.GetAssets(source, null);
             return availableAssets.Where(x => IsFontTtfOrOtf(x));
         }
 
@@ -34,60 +42,50 @@ namespace Avalonia.Media.Fonts
         /// Searches for font assets at a given location and only accepts assets that fit to a given filename expression.
         /// <para>File names can target multiple files with * wildcard. For example "FontFile*.ttf"</para>
         /// </summary>
-        /// <param name="fontFamilyKey"></param>
+        /// <param name="source"></param>
         /// <returns></returns>
-        private static IEnumerable<Uri> GetFontAssetsByExpression(FontFamilyKey fontFamilyKey)
+        private static IEnumerable<Uri> GetFontAssetsByExpression(Uri source)
         {
-            var (fileNameWithoutExtension, extension) = GetFileName(fontFamilyKey, out var location);
-            var filePattern = CreateFilePattern(fontFamilyKey, location, fileNameWithoutExtension);
+            var (fileNameWithoutExtension, extension) = GetFileName(source, out var location);
+            var filePattern = CreateFilePattern(source, location, fileNameWithoutExtension);
 
             var assetLoader = AvaloniaLocator.Current.GetRequiredService<IAssetLoader>();
-            var availableResources = assetLoader.GetAssets(location, fontFamilyKey.BaseUri);
+            var availableResources = assetLoader.GetAssets(location, null);
 
             return availableResources.Where(x => IsContainsFile(x, filePattern, extension));
         }
 
         private static (string fileNameWithoutExtension, string extension) GetFileName(
-            FontFamilyKey fontFamilyKey, out Uri location)
+            Uri source, out Uri location)
         {
-            if (fontFamilyKey.Source.IsAbsoluteResm())
+            if (source.IsAbsoluteResm())
             {
-                var fileName = GetFileNameAndExtension(fontFamilyKey.Source.GetUnescapeAbsolutePath(), '.');
+                var fileName = GetFileNameAndExtension(source.GetUnescapeAbsolutePath(), '.');
 
-                var uriLocation = fontFamilyKey.Source.GetUnescapeAbsoluteUri()
+                var uriLocation = source.GetUnescapeAbsoluteUri()
                     .Replace("." + fileName.fileNameWithoutExtension + fileName.extension, string.Empty);
                 location = new Uri(uriLocation, UriKind.RelativeOrAbsolute);
 
                 return fileName;
             }
 
-            var filename = GetFileNameAndExtension(fontFamilyKey.Source.OriginalString);
+            var filename = GetFileNameAndExtension(source.OriginalString);
             var fullFilename = filename.fileNameWithoutExtension + filename.extension;
 
-            if (fontFamilyKey.BaseUri != null)
-            {
-                var relativePath = fontFamilyKey.Source.OriginalString
-                    .Replace(fullFilename, string.Empty);
-
-                location = new Uri(fontFamilyKey.BaseUri, relativePath);
-            }
-            else
-            {
-                var uriString = fontFamilyKey.Source
-                    .GetUnescapeAbsoluteUri()
-                    .Replace(fullFilename, string.Empty);
-                location = new Uri(uriString);
-            }
+            var uriString = source
+                .GetUnescapeAbsoluteUri()
+                .Replace(fullFilename, string.Empty);
+            location = new Uri(uriString);
 
             return filename;
         }
 
         private static string CreateFilePattern(
-            FontFamilyKey fontFamilyKey, Uri location, string fileNameWithoutExtension)
+            Uri source, Uri location, string fileNameWithoutExtension)
         {
             var path = location.GetUnescapeAbsolutePath();
             var file = GetSubString(fileNameWithoutExtension, '*');
-            return fontFamilyKey.Source.IsAbsoluteResm()
+            return source.IsAbsoluteResm()
                 ? path + "." + file
                 : path + file;
         }

+ 33 - 0
src/Avalonia.Base/Media/Fonts/IFontCollection.cs

@@ -0,0 +1,33 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using Avalonia.Platform;
+
+namespace Avalonia.Media.Fonts
+{
+    public interface IFontCollection : IReadOnlyList<FontFamily>, IDisposable
+    {
+        /// <summary>
+        /// Get the font collection's key.
+        /// </summary>
+        Uri Key { get; }
+
+        /// <summary>
+        /// Initializes the font collection.
+        /// </summary>
+        /// <param name="fontManager">The font manager the collection is registered with.</param>
+        void Initialize(IFontManagerImpl fontManager);
+
+        /// <summary>
+        /// Try to get a glyph typeface for given parameters.
+        /// </summary>
+        /// <param name="familyName">The family name.</param>
+        /// <param name="style">The font style.</param>
+        /// <param name="weight">The font weight.</param>
+        /// <param name="stretch">The font stretch.</param>
+        /// <param name="glyphTypeface">The glyph typeface.</param>
+        /// <returns>Returns <c>true</c> if a glyph typface can be found; otherwise, <c>false</c></returns>
+        bool TryGetGlyphTypeface(string familyName, FontStyle style, FontWeight weight,
+            FontStretch stretch, [NotNullWhen(true)] out IGlyphTypeface? glyphTypeface);
+    }
+}

+ 107 - 0
src/Avalonia.Base/Media/Fonts/SystemFontCollection.cs

@@ -0,0 +1,107 @@
+using System;
+using System.Collections;
+using System.Collections.Concurrent;
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using Avalonia.Platform;
+
+namespace Avalonia.Media.Fonts
+{
+    internal class SystemFontCollection : IFontCollection
+    {
+        private readonly ConcurrentDictionary<string, ConcurrentDictionary<FontCollectionKey, IGlyphTypeface>> _glyphTypefaceCache = new();
+
+        private readonly FontManager _fontManager;
+        private readonly string[] _familyNames;
+
+        public SystemFontCollection(FontManager fontManager)
+        {
+            _fontManager = fontManager;
+            _familyNames = fontManager.PlatformImpl.GetInstalledFontFamilyNames();
+        }
+
+        public Uri Key => FontManager.SystemFontsKey;
+
+        public FontFamily this[int index]
+        {
+            get
+            {
+                var familyName = _familyNames[index];
+
+                return new FontFamily(familyName);
+            }
+        }
+
+        public int Count => _familyNames.Length;
+
+        public bool TryGetGlyphTypeface(string familyName, FontStyle style, FontWeight weight,
+            FontStretch stretch, [NotNullWhen(true)] out IGlyphTypeface? glyphTypeface)
+        {
+            if (familyName == FontFamily.DefaultFontFamilyName)
+            {
+                familyName = _fontManager.DefaultFontFamilyName;
+            }
+
+            var key = new FontCollectionKey(style, weight, stretch);
+
+            if (_glyphTypefaceCache.TryGetValue(familyName, out var glyphTypefaces))
+            {
+                if (glyphTypefaces.TryGetValue(key, out glyphTypeface))
+                {
+                    return true;
+                }
+                else
+                {
+                    if (_fontManager.PlatformImpl.TryCreateGlyphTypeface(familyName, style, weight, stretch, out glyphTypeface) &&
+                        glyphTypefaces.TryAdd(key, glyphTypeface))
+                    {
+                        return true;
+                    }
+                }
+            }
+
+            if (_fontManager.PlatformImpl.TryCreateGlyphTypeface(familyName, style, weight, stretch, out glyphTypeface))
+            {
+                glyphTypefaces = new ConcurrentDictionary<FontCollectionKey, IGlyphTypeface>();
+
+                if (glyphTypefaces.TryAdd(key, glyphTypeface) && _glyphTypefaceCache.TryAdd(familyName, glyphTypefaces))
+                {
+                    return true;
+                }
+            }
+
+            return false;
+        }
+
+        public void Initialize(IFontManagerImpl fontManager)
+        {
+            //We initialize the system font collection during construction.
+        }
+
+        IEnumerator IEnumerable.GetEnumerator()
+        {
+            return GetEnumerator();
+        }
+
+        public IEnumerator<FontFamily> GetEnumerator()
+        {
+            foreach (var familyName in _familyNames)
+            {
+                yield return new FontFamily(familyName);
+            }
+        }
+
+        void IDisposable.Dispose()
+        {
+            foreach (var glyphTypefaces in _glyphTypefaceCache.Values)
+            {
+                foreach (var pair in glyphTypefaces)
+                {
+                    pair.Value.Dispose();
+                }
+            }
+
+            GC.SuppressFinalize(this);
+        }
+    }
+}

+ 7 - 7
src/Avalonia.Base/Media/GlyphRun.cs

@@ -151,9 +151,9 @@ namespace Avalonia.Media
         }
 
         /// <summary>
-        ///     Gets or sets the conservative bounding box of the <see cref="GlyphRun"/>.
+        ///     Gets the conservative bounding box of the <see cref="GlyphRun"/>.
         /// </summary>
-        public Size Size => PlatformImpl.Item.Size;
+        public Rect Bounds => PlatformImpl.Item.Bounds;
 
         /// <summary>
         /// 
@@ -252,7 +252,7 @@ namespace Avalonia.Media
 
                 if (characterIndex > Metrics.LastCluster)
                 {
-                    return Size.Width;
+                    return Bounds.Width;
                 }
 
                 var glyphIndex = FindGlyphIndex(characterIndex);
@@ -287,7 +287,7 @@ namespace Avalonia.Media
 
                 if (characterIndex <= Metrics.FirstCluster)
                 {
-                    return Size.Width;
+                    return Bounds.Width;
                 }
 
                 for (var i = glyphIndex + 1; i < _glyphInfos.Count; i++)
@@ -295,7 +295,7 @@ namespace Avalonia.Media
                     distance += _glyphInfos[i].GlyphAdvance;
                 }
 
-                return Size.Width - distance;
+                return Bounds.Width - distance;
             }
         }
 
@@ -321,7 +321,7 @@ namespace Avalonia.Media
             }
 
             //After
-            if (distance >= Size.Width)
+            if (distance >= Bounds.Width)
             {
                 isInside = false;
 
@@ -354,7 +354,7 @@ namespace Avalonia.Media
             }
             else
             {
-                currentX = Size.Width;
+                currentX = Bounds.Width;
 
                 for (var index = _glyphInfos.Count - 1; index >= 0; index--)
                 {

+ 1 - 1
src/Avalonia.Base/Media/GlyphRunDrawing.cs

@@ -32,7 +32,7 @@
 
         public override Rect GetBounds()
         {
-            return GlyphRun != null ? new Rect(GlyphRun.Size) : default;
+            return GlyphRun != null ? GlyphRun.Bounds : default;
         }
     }
 }

+ 20 - 0
src/Avalonia.Base/Media/IGlyphTypeface.cs

@@ -6,6 +6,26 @@ namespace Avalonia.Media
     [Unstable]
     public interface IGlyphTypeface : IDisposable
     {
+        /// <summary>
+        /// Gets the family name for the <see cref="IGlyphTypeface"/> object.
+        /// </summary>
+        string FamilyName { get; }
+
+        /// <summary>
+        /// Gets the designed weight of the font represented by the <see cref="IGlyphTypeface"/> object.
+        /// </summary>
+        FontWeight Weight { get; }
+
+        /// <summary>
+        /// Gets the style for the <see cref="IGlyphTypeface"/> object.
+        /// </summary>
+        FontStyle Style { get; }
+
+        /// <summary>
+        /// Gets the <see cref="FontStretch"/> value for the <see cref="IGlyphTypeface"/> object.
+        /// </summary>
+        FontStretch Stretch { get; }
+
         /// <summary>
         ///     Gets the number of glyphs held by this glyph typeface. 
         /// </summary>

+ 0 - 2
src/Avalonia.Base/Media/Imaging/CroppedBitmap.cs

@@ -48,8 +48,6 @@ namespace Avalonia.Media.Imaging
 
         public CroppedBitmap()
         {
-            Source = null;
-            SourceRect = default;
         }
 
         public CroppedBitmap(IImage source, PixelRect sourceRect)

+ 1 - 1
src/Avalonia.Base/Media/TextDecoration.cs

@@ -223,7 +223,7 @@ namespace Avalonia.Media
                 if (intersections.Count > 0)
                 {
                     var last = baselineOrigin.X;
-                    var finalPos = last + glyphRun.Size.Width;
+                    var finalPos = last + glyphRun.Bounds.Width;
                     var end = last;
 
                     var points = new List<double>();

+ 2 - 2
src/Avalonia.Base/Media/TextFormatting/ShapedTextRun.cs

@@ -38,14 +38,14 @@ namespace Avalonia.Media.TextFormatting
 
         public override double Baseline => -TextMetrics.Ascent;
 
-        public override Size Size => GlyphRun.Size;
+        public override Size Size => GlyphRun.Bounds.Size;
 
         public GlyphRun GlyphRun => _glyphRun ??= CreateGlyphRun();
 
         /// <inheritdoc/>
         public override void Draw(DrawingContext drawingContext, Point origin)
         {
-            using (drawingContext.PushPreTransform(Matrix.CreateTranslation(origin)))
+            using (drawingContext.PushTransform(Matrix.CreateTranslation(origin)))
             {
                 if (GlyphRun.GlyphInfos.Count == 0)
                 {

+ 8 - 7
src/Avalonia.Base/Media/TextFormatting/TextCharacters.cs

@@ -122,13 +122,14 @@ namespace Avalonia.Media.TextFormatting
             if (matchFound)
             {
                 // Fallback found
-                var fallbackGlyphTypeface = fontManager.GetOrAddGlyphTypeface(fallbackTypeface);
-                                
-                if (TryGetShapeableLength(textSpan, fallbackGlyphTypeface, defaultGlyphTypeface, out count))
-                {                    
-                    return new UnshapedTextRun(text.Slice(0, count), defaultProperties.WithTypeface(fallbackTypeface),
-                        biDiLevel);
-                }                
+                if(fontManager.TryGetGlyphTypeface(fallbackTypeface, out var fallbackGlyphTypeface))
+                {
+                    if (TryGetShapeableLength(textSpan, fallbackGlyphTypeface, defaultGlyphTypeface, out count))
+                    {
+                        return new UnshapedTextRun(text.Slice(0, count), defaultProperties.WithTypeface(fallbackTypeface),
+                            biDiLevel);
+                    }
+                }          
             }
 
             // no fallback found

+ 1 - 1
src/Avalonia.Base/Media/TextFormatting/TextEllipsisHelper.cs

@@ -19,7 +19,7 @@ namespace Avalonia.Media.TextFormatting
             var collapsedLength = 0;
             var shapedSymbol = TextFormatterImpl.CreateSymbol(properties.Symbol, FlowDirection.LeftToRight);
 
-            if (properties.Width < shapedSymbol.GlyphRun.Size.Width)
+            if (properties.Width < shapedSymbol.GlyphRun.Bounds.Width)
             {
                 //Not enough space to fit in the symbol
                 return Array.Empty<TextRun>();

+ 1 - 1
src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs

@@ -658,7 +658,7 @@ namespace Avalonia.Media.TextFormatting
         /// Performs text wrapping returns a list of text lines.
         /// </summary>
         /// <param name="textRuns"></param>
-        /// <param name="canReuseTextRunList">Whether <see cref="textRuns"/> can be reused to store the split runs.</param>
+        /// <param name="canReuseTextRunList">Whether <see cref="TextRun"/> can be reused to store the split runs.</param>
         /// <param name="firstTextSourceIndex">The first text source index.</param>
         /// <param name="paragraphWidth">The paragraph width.</param>
         /// <param name="paragraphProperties">The text paragraph properties.</param>

+ 1 - 1
src/Avalonia.Base/Media/TextFormatting/TextLeadingPrefixCharacterEllipsis.cs

@@ -60,7 +60,7 @@ namespace Avalonia.Media.TextFormatting
             var currentWidth = 0.0;
             var shapedSymbol = TextFormatterImpl.CreateSymbol(Symbol, FlowDirection.LeftToRight);
 
-            if (Width < shapedSymbol.GlyphRun.Size.Width)
+            if (Width < shapedSymbol.GlyphRun.Bounds.Width)
             {
                 return Array.Empty<TextRun>();
             }

+ 3 - 3
src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs

@@ -423,7 +423,7 @@ namespace Avalonia.Media.TextFormatting
                     {
                         if (currentGlyphRun != null)
                         {
-                            currentDistance -= currentGlyphRun.Size.Width;
+                            currentDistance -= currentGlyphRun.Bounds.Width;
                         }
 
                         return currentDistance + distance;
@@ -477,7 +477,7 @@ namespace Avalonia.Media.TextFormatting
                         {
                             if (currentGlyphRun.IsLeftToRight || flowDirection == FlowDirection.RightToLeft)
                             {
-                                distance = currentGlyphRun.Size.Width;
+                                distance = currentGlyphRun.Bounds.Width;
                             }
 
                             return true;
@@ -1483,7 +1483,7 @@ namespace Avalonia.Media.TextFormatting
 
                     trailingWhitespaceLength += glyphRunMetrics.TrailingWhitespaceLength;
 
-                    var whitespaceWidth = glyphRun.Size.Width - glyphRunMetrics.Width;
+                    var whitespaceWidth = glyphRun.Bounds.Width - glyphRunMetrics.Width;
 
                     width -= whitespaceWidth;
                 }

+ 12 - 1
src/Avalonia.Base/Media/Typeface.cs

@@ -80,7 +80,18 @@ namespace Avalonia.Media
         /// <value>
         /// The glyph typeface.
         /// </value>
-        public IGlyphTypeface GlyphTypeface => FontManager.Current.GetOrAddGlyphTypeface(this);
+        public IGlyphTypeface GlyphTypeface
+        {
+            get
+            {
+                if(FontManager.Current.TryGetGlyphTypeface(this, out var glyphTypeface))
+                {
+                    return glyphTypeface;
+                }
+
+                throw new InvalidOperationException("Could not create glyphTypeface.");
+            }
+        }
 
         public static bool operator !=(Typeface a, Typeface b)
         {

+ 1 - 0
src/Avalonia.Base/Platform/IDrawingContextImpl.cs

@@ -128,6 +128,7 @@ namespace Avalonia.Platform
         /// Pushes an opacity value.
         /// </summary>
         /// <param name="opacity">The opacity.</param>
+        /// <param name="bounds">where to apply the opacity.</param>
         void PushOpacity(double opacity, Rect bounds);
 
         /// <summary>

+ 23 - 7
src/Avalonia.Base/Platform/IFontManagerImpl.cs

@@ -1,5 +1,6 @@
-using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
 using System.Globalization;
+using System.IO;
 using Avalonia.Media;
 using Avalonia.Metadata;
 
@@ -17,7 +18,7 @@ namespace Avalonia.Platform
         ///     Get all installed fonts in the system.
         /// <param name="checkForUpdates">If <c>true</c> the font collection is updated.</param>
         /// </summary>
-        IEnumerable<string> GetInstalledFontFamilyNames(bool checkForUpdates = false);
+        string[] GetInstalledFontFamilyNames(bool checkForUpdates = false);
 
         /// <summary>
         ///     Tries to match a specified character to a typeface that supports specified font properties.
@@ -37,12 +38,27 @@ namespace Avalonia.Platform
             FontFamily? fontFamily, CultureInfo? culture, out Typeface typeface);
 
         /// <summary>
-        ///     Creates a glyph typeface.
+        ///     Tries to get a glyph typeface for specified parameters.
         /// </summary>
-        /// <param name="typeface">The typeface.</param>
-        /// <returns>0
-        ///     The created glyph typeface. Can be <c>Null</c> if it was not possible to create a glyph typeface.
+        /// <param name="familyName">The family name.</param>
+        /// <param name="style">The font style.</param>
+        /// <param name="weight">The font weiht.</param>
+        /// <param name="stretch">The font stretch.</param>
+        /// <param name="glyphTypeface">The created glyphTypeface</param>
+        /// <returns>
+        ///     <c>True</c>, if the <see cref="IFontManagerImpl"/> could create the glyph typeface, <c>False</c> otherwise.
+        /// </returns>
+        bool TryCreateGlyphTypeface(string familyName, FontStyle style, FontWeight weight,
+            FontStretch stretch, [NotNullWhen(returnValue: true)] out IGlyphTypeface? glyphTypeface);
+
+        /// <summary>
+        ///     Tries to create a glyph typeface from specified stream.
+        /// </summary>
+        /// <param name="stream">A stream that holds the font's data.</param>
+        /// <param name="glyphTypeface">The created glyphTypeface</param>
+        /// <returns>
+        ///     <c>True</c>, if the <see cref="IFontManagerImpl"/> could create the glyph typeface, <c>False</c> otherwise.
         /// </returns>
-        IGlyphTypeface CreateGlyphTypeface(Typeface typeface);
+        bool TryCreateGlyphTypeface(Stream stream, [NotNullWhen(returnValue: true)] out IGlyphTypeface? glyphTypeface);
     }
 }

この差分においてかなりの量のファイルが変更されているため、一部のファイルを表示していません